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,320 @@
1
+ """CI-enforceable rule runner.
2
+
3
+ Provides a synchronous, exit-code-driven interface for running rules
4
+ in CI/CD pipelines. Supports diff-only scanning, SARIF output, and
5
+ GitHub Actions annotations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ import subprocess
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+
16
+ from attocode_intel.rules.model import EnrichedFinding, RuleSeverity
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Severity ordering for threshold comparison
21
+ _SEV_ORDER = {
22
+ RuleSeverity.CRITICAL: 0,
23
+ RuleSeverity.HIGH: 1,
24
+ RuleSeverity.MEDIUM: 2,
25
+ RuleSeverity.LOW: 3,
26
+ RuleSeverity.INFO: 4,
27
+ }
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class CIResult:
32
+ """Result of a CI scan run."""
33
+
34
+ findings: list[EnrichedFinding]
35
+ exit_code: int = 0
36
+ files_scanned: int = 0
37
+ rules_applied: int = 0
38
+ findings_above_threshold: int = 0
39
+ confidence: dict = field(default_factory=dict)
40
+
41
+ @property
42
+ def passed(self) -> bool:
43
+ return self.exit_code == 0
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class CIConfig:
48
+ """Configuration for CI scans, loadable from .attocode/ci.yaml."""
49
+
50
+ fail_on: RuleSeverity = RuleSeverity.HIGH
51
+ baseline: str = "" # git ref for diff-only
52
+ exclude_rules: list[str] = field(default_factory=list)
53
+ sarif_output: str = "" # path to write SARIF
54
+ min_confidence: float = 0.5
55
+ max_findings: int = 200
56
+
57
+
58
+ def load_ci_config(project_dir: str) -> CIConfig:
59
+ """Load CI config from .attocode/ci.yaml if it exists."""
60
+ config_path = Path(project_dir) / ".attocode" / "ci.yaml"
61
+ if not config_path.is_file():
62
+ return CIConfig()
63
+
64
+ try:
65
+ import yaml # type: ignore[import-untyped]
66
+ data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
67
+ if not isinstance(data, dict):
68
+ return CIConfig()
69
+
70
+ cfg = CIConfig()
71
+ if "fail_on" in data:
72
+ sev = str(data["fail_on"]).lower()
73
+ if sev in {s.value for s in RuleSeverity}:
74
+ cfg.fail_on = RuleSeverity(sev)
75
+ cfg.baseline = str(data.get("baseline", ""))
76
+ cfg.exclude_rules = list(data.get("exclude_rules", []))
77
+ cfg.sarif_output = str(data.get("sarif_output", ""))
78
+ cfg.min_confidence = float(data.get("min_confidence", 0.5))
79
+ cfg.max_findings = int(data.get("max_findings", 200))
80
+ return cfg
81
+ except Exception as exc:
82
+ logger.warning("Failed to load CI config: %s", exc)
83
+ return CIConfig()
84
+
85
+
86
+ def get_changed_files(
87
+ base_ref: str,
88
+ project_dir: str,
89
+ ) -> list[str]:
90
+ """Get files changed since *base_ref* using git diff."""
91
+ try:
92
+ result = subprocess.run(
93
+ ["git", "diff", "--name-only", "--diff-filter=ACMR", base_ref],
94
+ capture_output=True, text=True, cwd=project_dir,
95
+ timeout=30,
96
+ )
97
+ if result.returncode != 0:
98
+ logger.warning("git diff failed: %s", result.stderr.strip())
99
+ return []
100
+
101
+ files = []
102
+ for line in result.stdout.strip().splitlines():
103
+ abs_path = os.path.join(project_dir, line)
104
+ if os.path.isfile(abs_path):
105
+ files.append(abs_path)
106
+ return files
107
+ except (OSError, subprocess.TimeoutExpired) as exc:
108
+ logger.warning("Failed to get changed files: %s", exc)
109
+ return []
110
+
111
+
112
+ def get_changed_lines(
113
+ base_ref: str,
114
+ file_path: str,
115
+ project_dir: str,
116
+ ) -> set[int]:
117
+ """Get line numbers that changed in *file_path* since *base_ref*."""
118
+ try:
119
+ rel_path = os.path.relpath(file_path, project_dir)
120
+ result = subprocess.run(
121
+ ["git", "diff", "-U0", base_ref, "--", rel_path],
122
+ capture_output=True, text=True, cwd=project_dir,
123
+ timeout=30,
124
+ )
125
+ if result.returncode != 0:
126
+ return set()
127
+
128
+ import re as _re
129
+ _HUNK_RE = _re.compile(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
130
+
131
+ changed: set[int] = set()
132
+ for line in result.stdout.splitlines():
133
+ m = _HUNK_RE.match(line)
134
+ if m:
135
+ start = int(m.group(1))
136
+ count = int(m.group(2)) if m.group(2) is not None else 1
137
+ changed.update(range(start, start + count))
138
+ return changed
139
+ except (OSError, subprocess.TimeoutExpired, ValueError):
140
+ return set()
141
+
142
+
143
+ class CIRunner:
144
+ """Run rules in CI mode with exit codes and structured output."""
145
+
146
+ def __init__(self, project_dir: str, config: CIConfig | None = None) -> None:
147
+ self.project_dir = project_dir
148
+ self.config = config or load_ci_config(project_dir)
149
+
150
+ def run(
151
+ self,
152
+ files: list[str] | None = None,
153
+ path: str = "",
154
+ language: str = "",
155
+ category: str = "",
156
+ *,
157
+ diff_only: bool = False,
158
+ ) -> CIResult:
159
+ """Execute rules in CI mode.
160
+
161
+ Args:
162
+ files: Explicit file list to scan.
163
+ path: Directory to scan (relative to project root).
164
+ language: Filter rules to this language.
165
+ category: Filter by category.
166
+ diff_only: Only report findings on changed lines since baseline.
167
+
168
+ Returns:
169
+ CIResult with findings and exit code.
170
+ """
171
+ from attocode_intel.rules.registry import RuleRegistry
172
+ from attocode_intel.rules.loader import load_builtin_rules, load_user_rules
173
+ from attocode_intel.rules.packs.pack_loader import load_all_packs
174
+ from attocode_intel.rules.executor import execute_rules
175
+ from attocode_intel.rules.enricher import enrich_findings
176
+ from attocode_intel.rules.filters.pipeline import run_pipeline
177
+
178
+ # Build registry
179
+ reg = RuleRegistry()
180
+ reg.register_many(load_builtin_rules())
181
+ pack_manifests, pack_rules = load_all_packs(self.project_dir)
182
+ reg.register_many(pack_rules)
183
+ user_rules = load_user_rules(self.project_dir)
184
+ reg.register_many(user_rules)
185
+
186
+ # Query applicable rules
187
+ from attocode_intel import confidence
188
+ from attocode_intel.confidence.settings import workspace
189
+
190
+ with workspace(self.project_dir):
191
+ threshold = confidence.rule_threshold(self.config.min_confidence)
192
+ rules = reg.query(
193
+ language=language,
194
+ category=category,
195
+ min_confidence=threshold,
196
+ )
197
+
198
+ # Exclude rules from config
199
+ if self.config.exclude_rules:
200
+ exclude = set(self.config.exclude_rules)
201
+ rules = [r for r in rules if r.qualified_id not in exclude and r.id not in exclude]
202
+
203
+ # Collect files
204
+ if diff_only and self.config.baseline:
205
+ file_list = files or get_changed_files(self.config.baseline, self.project_dir)
206
+ else:
207
+ from attocode_intel.tools.rule_tools import _collect_files
208
+ file_list = _collect_files(files, path, self.project_dir)
209
+
210
+ if not file_list or not rules:
211
+ return CIResult(findings=[], files_scanned=len(file_list or []), rules_applied=len(rules))
212
+
213
+ # Execute
214
+ findings = execute_rules(file_list, rules, project_dir=self.project_dir)
215
+ findings = run_pipeline(findings, min_confidence=self.config.min_confidence,
216
+ project_dir=self.project_dir)
217
+ enrich_findings(findings, project_dir=self.project_dir)
218
+
219
+ # Diff-only filtering: only keep findings on changed lines
220
+ if diff_only and self.config.baseline:
221
+ changed_lines_cache: dict[str, set[int]] = {}
222
+ filtered = []
223
+ for f in findings:
224
+ abs_path = f.file if os.path.isabs(f.file) else os.path.join(self.project_dir, f.file)
225
+ if abs_path not in changed_lines_cache:
226
+ changed_lines_cache[abs_path] = get_changed_lines(
227
+ self.config.baseline, abs_path, self.project_dir,
228
+ )
229
+ if f.line in changed_lines_cache[abs_path]:
230
+ filtered.append(f)
231
+ findings = filtered
232
+
233
+ # Cap findings
234
+ findings = findings[:self.config.max_findings]
235
+
236
+ # Compute exit code based on threshold
237
+ threshold = _SEV_ORDER.get(self.config.fail_on, 1)
238
+ above = [f for f in findings if _SEV_ORDER.get(RuleSeverity(f.severity), 4) <= threshold]
239
+
240
+ result = CIResult(
241
+ findings=findings,
242
+ exit_code=1 if above else 0,
243
+ files_scanned=len(file_list),
244
+ rules_applied=len(rules),
245
+ findings_above_threshold=len(above),
246
+ confidence={k: v for k, v in confidence.last_report().items() if k != "findings"},
247
+ )
248
+
249
+ # Write SARIF if configured
250
+ if self.config.sarif_output:
251
+ self._write_sarif(findings)
252
+
253
+ return result
254
+
255
+ def _write_sarif(self, findings: list[EnrichedFinding]) -> None:
256
+ """Write findings as SARIF to the configured output path."""
257
+ from attocode_intel.rules.sarif import findings_to_sarif, sarif_to_json
258
+
259
+ sarif = findings_to_sarif(findings)
260
+ output_path = Path(self.config.sarif_output)
261
+ if not output_path.is_absolute():
262
+ output_path = Path(self.project_dir) / output_path
263
+ output_path.parent.mkdir(parents=True, exist_ok=True)
264
+ output_path.write_text(sarif_to_json(sarif), encoding="utf-8")
265
+ logger.info("SARIF output written to %s", output_path)
266
+
267
+
268
+ def format_github_annotations(findings: list[EnrichedFinding]) -> str:
269
+ """Format findings as GitHub Actions annotation commands.
270
+
271
+ Output format: ``::error file=path,line=N::message``
272
+ """
273
+ _LEVEL = {
274
+ RuleSeverity.CRITICAL: "error",
275
+ RuleSeverity.HIGH: "error",
276
+ RuleSeverity.MEDIUM: "warning",
277
+ RuleSeverity.LOW: "notice",
278
+ RuleSeverity.INFO: "notice",
279
+ }
280
+ lines: list[str] = []
281
+ for f in findings:
282
+ level = _LEVEL.get(RuleSeverity(f.severity), "warning")
283
+ msg = f"[{f.rule_id}] {f.description}"
284
+ # GitHub Actions requires percent-encoding for %, \r, \n in messages
285
+ msg = msg.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
286
+ lines.append(f"::{level} file={f.file},line={f.line}::{msg}")
287
+ return "\n".join(lines)
288
+
289
+
290
+ def format_ci_summary(result: CIResult) -> str:
291
+ """Format a CI result as a human-readable summary."""
292
+ status = "PASS" if result.passed else "FAIL"
293
+ lines = [
294
+ f"## Rule Analysis: {status}",
295
+ f"Files scanned: {result.files_scanned} | Rules applied: {result.rules_applied}",
296
+ f"Findings: {len(result.findings)} total, {result.findings_above_threshold} above threshold",
297
+ ]
298
+ audit = result.confidence
299
+ if audit and audit.get("scorer") != "off":
300
+ lines.append(
301
+ f"Confidence: {audit['scorer']} ({audit['mode']}); "
302
+ f"{audit['estimated']}/{audit['total']} scored, {audit['fallback']} fallback, "
303
+ f"{audit['skipped']} skipped by cap (baseline retained)."
304
+ )
305
+
306
+ if result.findings:
307
+ # Group by severity
308
+ by_sev: dict[str, int] = {}
309
+ for f in result.findings:
310
+ by_sev[f.severity] = by_sev.get(f.severity, 0) + 1
311
+ lines.append("Breakdown: " + ", ".join(f"{s}: {c}" for s, c in sorted(by_sev.items())))
312
+
313
+ # Top findings
314
+ lines.append("")
315
+ for f in result.findings[:10]:
316
+ lines.append(f"- [{f.severity}] {f.file}:{f.line} — {f.description[:80]}")
317
+ if len(result.findings) > 10:
318
+ lines.append(f"... and {len(result.findings) - 10} more")
319
+
320
+ return "\n".join(lines)
@@ -0,0 +1,293 @@
1
+ """Boolean pattern combinators for composite rule matching.
2
+
3
+ Enables Semgrep-style ``pattern-either``, ``pattern-not``, and
4
+ ``pattern-inside`` composition without requiring AST parsing.
5
+
6
+ All combinators operate on a ``MatchContext`` that provides the
7
+ current line, surrounding lines, and file content for scope analysis.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+
16
+ from attocode_intel.rules.metavar import compile_metavar_pattern, has_metavars
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Match context
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class MatchContext:
26
+ """Context passed to pattern nodes during evaluation."""
27
+
28
+ line: str # current line being evaluated
29
+ line_no: int # 1-based line number
30
+ all_lines: list[str] # full file content (0-indexed)
31
+ captures: dict[str, str] = field(default_factory=dict)
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Pattern node hierarchy
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ class PatternNode(ABC):
40
+ """Abstract base for all pattern nodes in a composite rule."""
41
+
42
+ @abstractmethod
43
+ def evaluate(self, ctx: MatchContext) -> bool:
44
+ """Return True if this pattern matches in the given context."""
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class RegexNode(PatternNode):
49
+ """Matches a single line against a compiled regex."""
50
+
51
+ pattern: re.Pattern[str]
52
+ metavar_names: list[str] = field(default_factory=list)
53
+
54
+ def evaluate(self, ctx: MatchContext) -> bool:
55
+ m = self.pattern.search(ctx.line)
56
+ if m and self.metavar_names:
57
+ ctx.captures.update(
58
+ {k: v for k, v in m.groupdict().items() if v is not None}
59
+ )
60
+ return bool(m)
61
+
62
+
63
+ @dataclass(slots=True)
64
+ class EitherNode(PatternNode):
65
+ """OR combinator — matches if ANY child matches."""
66
+
67
+ children: list[PatternNode]
68
+
69
+ def evaluate(self, ctx: MatchContext) -> bool:
70
+ return any(child.evaluate(ctx) for child in self.children)
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class AllNode(PatternNode):
75
+ """AND combinator — matches if ALL children match the same line."""
76
+
77
+ children: list[PatternNode]
78
+
79
+ def evaluate(self, ctx: MatchContext) -> bool:
80
+ return all(child.evaluate(ctx) for child in self.children)
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class NotNode(PatternNode):
85
+ """Negation — matches if the child does NOT match the current line."""
86
+
87
+ child: PatternNode
88
+
89
+ def evaluate(self, ctx: MatchContext) -> bool:
90
+ return not self.child.evaluate(ctx)
91
+
92
+
93
+ # Function definition patterns — used as scope boundaries
94
+ _FUNC_DEF_RE = re.compile(
95
+ r"^(?:def |func |function |fn |"
96
+ r"(?:public|private|protected|internal)\s+(?:static\s+)?\w+\s+\w+\(|"
97
+ r"(?:export\s+)?(?:async\s+)?function\s)"
98
+ )
99
+
100
+
101
+ def _indent_level(line: str) -> int:
102
+ """Return the number of leading whitespace characters."""
103
+ return len(line) - len(line.lstrip())
104
+
105
+
106
+ @dataclass(slots=True)
107
+ class InsideNode(PatternNode):
108
+ """Scope constraint — matches only if a scope pattern is found above.
109
+
110
+ Scans backward from the current line looking for a line that matches
111
+ the scope pattern within ``max_distance`` lines. Uses two heuristics
112
+ to avoid cross-scope false positives:
113
+
114
+ 1. The scope line must have *less* indentation than the current line
115
+ (the current line is "inside" the scope block).
116
+ 2. Scanning stops at function definition boundaries to prevent
117
+ matching scope patterns from preceding functions.
118
+ """
119
+
120
+ scope_pattern: re.Pattern[str]
121
+ max_distance: int = 50
122
+
123
+ def evaluate(self, ctx: MatchContext) -> bool:
124
+ cur_indent = _indent_level(ctx.line)
125
+ start = max(0, ctx.line_no - 1 - self.max_distance)
126
+ end = ctx.line_no - 1 # exclude current line (0-indexed)
127
+ for i in range(end - 1, start - 1, -1):
128
+ scan_line = ctx.all_lines[i]
129
+ stripped = scan_line.strip()
130
+ if not stripped:
131
+ continue
132
+
133
+ # Check scope pattern BEFORE function boundary
134
+ # (the func def line itself may be the scope pattern)
135
+ if self.scope_pattern.search(scan_line):
136
+ if _indent_level(scan_line) < cur_indent:
137
+ return True
138
+
139
+ # Stop at function boundaries (different scope)
140
+ if _FUNC_DEF_RE.match(stripped):
141
+ return False
142
+ return False
143
+
144
+
145
+ @dataclass(slots=True)
146
+ class NotInsideNode(PatternNode):
147
+ """Inverse scope — matches only if the scope pattern is NOT found above."""
148
+
149
+ scope_pattern: re.Pattern[str]
150
+ max_distance: int = 50
151
+
152
+ def evaluate(self, ctx: MatchContext) -> bool:
153
+ cur_indent = _indent_level(ctx.line)
154
+ start = max(0, ctx.line_no - 1 - self.max_distance)
155
+ end = ctx.line_no - 1
156
+ for i in range(end - 1, start - 1, -1):
157
+ scan_line = ctx.all_lines[i]
158
+ stripped = scan_line.strip()
159
+ if not stripped:
160
+ continue
161
+ # Check scope pattern BEFORE function boundary
162
+ # (the func def line itself may be the scope pattern)
163
+ if self.scope_pattern.search(scan_line):
164
+ if _indent_level(scan_line) < cur_indent:
165
+ return False
166
+ if _FUNC_DEF_RE.match(stripped):
167
+ return True # hit function boundary — not inside
168
+ return True
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Composite pattern (wraps primary + constraints)
173
+ # ---------------------------------------------------------------------------
174
+
175
+
176
+ @dataclass(slots=True)
177
+ class CompositePattern:
178
+ """A primary pattern with boolean constraints.
179
+
180
+ The primary pattern must match the current line. Then all constraints
181
+ (pattern-not, pattern-inside, pattern-not-inside) are evaluated.
182
+ All constraints must pass for the composite to match.
183
+ """
184
+
185
+ primary: PatternNode
186
+ constraints: list[PatternNode] = field(default_factory=list)
187
+
188
+ def evaluate(self, ctx: MatchContext) -> bool:
189
+ if not self.primary.evaluate(ctx):
190
+ return False
191
+ return all(c.evaluate(ctx) for c in self.constraints)
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Builder — parses YAML structure into pattern tree
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ def _compile_pattern(pattern_str: str) -> tuple[re.Pattern[str], list[str]]:
200
+ """Compile a pattern string, handling metavariables if present."""
201
+ if has_metavars(pattern_str):
202
+ return compile_metavar_pattern(pattern_str)
203
+ return re.compile(pattern_str), []
204
+
205
+
206
+ def build_composite_from_yaml(data: list[dict] | dict) -> CompositePattern | None:
207
+ """Build a CompositePattern from YAML ``patterns`` structure.
208
+
209
+ Supported YAML keys within the patterns list:
210
+ - ``pattern``: primary regex (first one found becomes primary)
211
+ - ``pattern-either``: list of alternative patterns (becomes primary)
212
+ - ``pattern-not``: line must NOT match this regex
213
+ - ``pattern-inside``: a scope pattern must exist above the current line
214
+ - ``pattern-not-inside``: a scope pattern must NOT exist above
215
+
216
+ Example YAML:
217
+ patterns:
218
+ - pattern: 'fmt\\.Sprintf\\s*\\('
219
+ - pattern-inside: 'for\\s.*\\{'
220
+ - pattern-not: '// nolint'
221
+ """
222
+ if isinstance(data, dict):
223
+ # Single pattern-either at top level
224
+ if "pattern-either" in data:
225
+ either_pats = data["pattern-either"]
226
+ if not isinstance(either_pats, list):
227
+ return None
228
+ children = []
229
+ all_metavars: list[str] = []
230
+ for p in either_pats:
231
+ pat_str = str(p)
232
+ compiled, mvars = _compile_pattern(pat_str)
233
+ children.append(RegexNode(pattern=compiled, metavar_names=mvars))
234
+ all_metavars.extend(mvars)
235
+ return CompositePattern(primary=EitherNode(children=children))
236
+ return None
237
+
238
+ if not isinstance(data, list):
239
+ return None
240
+
241
+ primary: PatternNode | None = None
242
+ constraints: list[PatternNode] = []
243
+ all_metavars: list[str] = []
244
+
245
+ for item in data:
246
+ if not isinstance(item, dict):
247
+ continue
248
+
249
+ if "pattern" in item:
250
+ pat_str = str(item["pattern"])
251
+ compiled, mvars = _compile_pattern(pat_str)
252
+ if primary is None:
253
+ primary = RegexNode(pattern=compiled, metavar_names=mvars)
254
+ all_metavars.extend(mvars)
255
+ else:
256
+ # Additional patterns act as AND constraints
257
+ constraints.append(RegexNode(pattern=compiled, metavar_names=mvars))
258
+
259
+ elif "pattern-either" in item:
260
+ either_pats = item["pattern-either"]
261
+ if isinstance(either_pats, list):
262
+ children = []
263
+ for p in either_pats:
264
+ c, mv = _compile_pattern(str(p))
265
+ children.append(RegexNode(pattern=c, metavar_names=mv))
266
+ all_metavars.extend(mv)
267
+ node = EitherNode(children=children)
268
+ if primary is None:
269
+ primary = node
270
+ else:
271
+ constraints.append(node)
272
+
273
+ elif "pattern-not" in item:
274
+ pat_str = str(item["pattern-not"])
275
+ compiled, mvars = _compile_pattern(pat_str)
276
+ constraints.append(NotNode(child=RegexNode(pattern=compiled, metavar_names=mvars)))
277
+
278
+ elif "pattern-inside" in item:
279
+ pat_str = str(item["pattern-inside"])
280
+ compiled, _ = _compile_pattern(pat_str)
281
+ max_dist = int(item.get("max-distance", 50))
282
+ constraints.append(InsideNode(scope_pattern=compiled, max_distance=max_dist))
283
+
284
+ elif "pattern-not-inside" in item:
285
+ pat_str = str(item["pattern-not-inside"])
286
+ compiled, _ = _compile_pattern(pat_str)
287
+ max_dist = int(item.get("max-distance", 50))
288
+ constraints.append(NotInsideNode(scope_pattern=compiled, max_distance=max_dist))
289
+
290
+ if primary is None:
291
+ return None
292
+
293
+ return CompositePattern(primary=primary, constraints=constraints)