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,332 @@
1
+ """Rule profiling and confidence calibration.
2
+
3
+ Tracks per-rule execution time, match counts, and true/false positive
4
+ feedback to calibrate confidence scores over time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import threading
12
+ import time
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class RuleStats:
21
+ """Per-rule profiling statistics."""
22
+
23
+ rule_id: str
24
+ total_time_ms: float = 0.0
25
+ match_count: int = 0
26
+ files_scanned: int = 0
27
+ true_positives: int = 0
28
+ false_positives: int = 0
29
+
30
+ @property
31
+ def calibrated_confidence(self) -> float | None:
32
+ """Compute calibrated confidence from TP/FP feedback.
33
+
34
+ Returns None if insufficient data (<5 samples).
35
+ """
36
+ total = self.true_positives + self.false_positives
37
+ if total < 5:
38
+ return None
39
+ return self.true_positives / total
40
+
41
+ @property
42
+ def false_positive_rate(self) -> float | None:
43
+ total = self.true_positives + self.false_positives
44
+ if total == 0:
45
+ return None
46
+ return self.false_positives / total
47
+
48
+
49
+ class RuleProfiler:
50
+ """Thread-safe rule profiler with per-rule timing and feedback tracking."""
51
+
52
+ def __init__(self) -> None:
53
+ self._stats: dict[str, RuleStats] = {}
54
+ self._lock = threading.Lock()
55
+ self._timers: dict[tuple[str, int], float] = {} # (rule_id, thread_id) -> start
56
+
57
+ def _ensure(self, rule_id: str) -> RuleStats:
58
+ if rule_id not in self._stats:
59
+ self._stats[rule_id] = RuleStats(rule_id=rule_id)
60
+ return self._stats[rule_id]
61
+
62
+ def start(self, rule_id: str) -> None:
63
+ """Start timing a rule execution."""
64
+ key = (rule_id, threading.get_ident())
65
+ with self._lock:
66
+ self._timers[key] = time.monotonic()
67
+
68
+ def stop(self, rule_id: str) -> None:
69
+ """Stop timing and record elapsed time."""
70
+ key = (rule_id, threading.get_ident())
71
+ with self._lock:
72
+ start = self._timers.pop(key, None)
73
+ if start is None:
74
+ return
75
+ elapsed_ms = (time.monotonic() - start) * 1000
76
+ with self._lock:
77
+ stats = self._ensure(rule_id)
78
+ stats.total_time_ms += elapsed_ms
79
+
80
+ def record_match(self, rule_id: str) -> None:
81
+ """Record a rule match."""
82
+ with self._lock:
83
+ self._ensure(rule_id).match_count += 1
84
+
85
+ def record_file_scanned(self, rule_id: str) -> None:
86
+ """Record that a rule was evaluated against a file."""
87
+ with self._lock:
88
+ self._ensure(rule_id).files_scanned += 1
89
+
90
+ def record_feedback(self, rule_id: str, *, is_true_positive: bool) -> None:
91
+ """Record TP/FP feedback for confidence calibration."""
92
+ with self._lock:
93
+ stats = self._ensure(rule_id)
94
+ if is_true_positive:
95
+ stats.true_positives += 1
96
+ else:
97
+ stats.false_positives += 1
98
+
99
+ def get_stats(self, rule_id: str = "") -> dict[str, RuleStats]:
100
+ """Get stats for a specific rule or all rules."""
101
+ with self._lock:
102
+ if rule_id:
103
+ s = self._stats.get(rule_id)
104
+ return {rule_id: s} if s else {}
105
+ return dict(self._stats)
106
+
107
+ def reset(self) -> None:
108
+ """Clear all profiling data."""
109
+ with self._lock:
110
+ self._stats.clear()
111
+ self._timers.clear()
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Persistent feedback store
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ class FeedbackStore:
120
+ """Persistent rule-quality state stored in .attocode/rule_feedback.json.
121
+
122
+ Tracks both TP/FP feedback (for confidence calibration) and per-rule
123
+ scan/match counters plus hygiene-applied disable state (consumed by
124
+ ``rules.hygiene``). One JSON file keeps related metadata together so
125
+ auto-prune decisions persist across sessions.
126
+ """
127
+
128
+ def __init__(self, project_dir: str) -> None:
129
+ self._path = Path(project_dir) / ".attocode" / "rule_feedback.json"
130
+ self._data: dict[str, dict[str, object]] = {}
131
+ # Single in-process lock guards both the in-memory map and the
132
+ # read-modify-write JSON file. Cross-process safety would need a
133
+ # ``filelock``; in-process MCP/HTTP concurrency is the immediate
134
+ # need here (record_session() fires on every analyze() call).
135
+ self._lock = threading.Lock()
136
+ self._load()
137
+
138
+ def _load(self) -> None:
139
+ if self._path.is_file():
140
+ try:
141
+ self._data = json.loads(self._path.read_text(encoding="utf-8"))
142
+ except (json.JSONDecodeError, OSError) as exc:
143
+ logger.warning("Failed to load rule feedback: %s", exc)
144
+ self._data = {}
145
+
146
+ def _save_locked(self) -> None:
147
+ """Persist to disk. Caller must hold ``self._lock``."""
148
+ try:
149
+ self._path.parent.mkdir(parents=True, exist_ok=True)
150
+ self._path.write_text(
151
+ json.dumps(self._data, indent=2, sort_keys=True),
152
+ encoding="utf-8",
153
+ )
154
+ except OSError as exc:
155
+ logger.warning("Failed to save rule feedback: %s", exc)
156
+
157
+ @staticmethod
158
+ def _entry_int(entry: dict[str, object], key: str) -> int:
159
+ value = entry.get(key, 0)
160
+ return int(value) if isinstance(value, (int, float)) else 0
161
+
162
+ def _ensure_locked(self, rule_id: str) -> dict[str, object]:
163
+ """Caller must hold ``self._lock``."""
164
+ return self._data.setdefault(rule_id, {"tp": 0, "fp": 0})
165
+
166
+ def record(self, rule_id: str, *, is_true_positive: bool) -> None:
167
+ """Record a TP/FP observation and persist."""
168
+ with self._lock:
169
+ entry = self._ensure_locked(rule_id)
170
+ if is_true_positive:
171
+ entry["tp"] = self._entry_int(entry, "tp") + 1
172
+ else:
173
+ entry["fp"] = self._entry_int(entry, "fp") + 1
174
+ total = self._entry_int(entry, "tp") + self._entry_int(entry, "fp")
175
+ if total >= 5:
176
+ tp = self._entry_int(entry, "tp")
177
+ entry["calibrated"] = round(tp / total, 3)
178
+ self._save_locked()
179
+
180
+ def record_session(
181
+ self,
182
+ rule_ids: list[str],
183
+ matches: dict[str, int],
184
+ *,
185
+ files_scanned: int = 0,
186
+ ) -> None:
187
+ """Record one analyze-session's scan + match counters across rules.
188
+
189
+ Each rule that was applied in the session has its ``scans`` counter
190
+ incremented by 1 (one session = one scan, irrespective of file count
191
+ — matching the roadmap's "0 matches across 10+ scanned repos"
192
+ framing). Matches are added cumulatively. ``files_scanned`` is
193
+ recorded as a rolling total per-rule for diagnostics only.
194
+ """
195
+ if not rule_ids:
196
+ return
197
+ with self._lock:
198
+ for qid in rule_ids:
199
+ entry = self._ensure_locked(qid)
200
+ entry["scans"] = self._entry_int(entry, "scans") + 1
201
+ if files_scanned:
202
+ entry["files_scanned"] = (
203
+ self._entry_int(entry, "files_scanned") + files_scanned
204
+ )
205
+ entry["matches"] = (
206
+ self._entry_int(entry, "matches") + int(matches.get(qid, 0))
207
+ )
208
+ self._save_locked()
209
+
210
+ def set_disabled(self, rule_id: str, reason: str) -> None:
211
+ """Mark a rule disabled with a reason (e.g. 'dead', 'noisy')."""
212
+ with self._lock:
213
+ entry = self._ensure_locked(rule_id)
214
+ entry["disabled"] = True
215
+ entry["disabled_reason"] = reason
216
+ self._save_locked()
217
+
218
+ def clear_disabled(self, rule_id: str) -> None:
219
+ """Re-enable a previously hygiene-disabled rule."""
220
+ with self._lock:
221
+ entry = self._data.get(rule_id)
222
+ if entry is None:
223
+ return
224
+ entry["disabled"] = False
225
+ entry["disabled_reason"] = ""
226
+ self._save_locked()
227
+
228
+ def is_disabled(self, rule_id: str) -> bool:
229
+ with self._lock:
230
+ entry = self._data.get(rule_id, {})
231
+ return bool(entry.get("disabled", False))
232
+
233
+ def get_disabled_reason(self, rule_id: str) -> str:
234
+ with self._lock:
235
+ entry = self._data.get(rule_id, {})
236
+ value = entry.get("disabled_reason", "")
237
+ return value if isinstance(value, str) else ""
238
+
239
+ def get_scan_count(self, rule_id: str) -> int:
240
+ with self._lock:
241
+ return self._entry_int(self._data.get(rule_id, {}), "scans")
242
+
243
+ def get_match_count(self, rule_id: str) -> int:
244
+ with self._lock:
245
+ return self._entry_int(self._data.get(rule_id, {}), "matches")
246
+
247
+ def get_calibrated_confidence(self, rule_id: str) -> float | None:
248
+ """Get calibrated confidence for a rule, or None if insufficient data."""
249
+ with self._lock:
250
+ entry = self._data.get(rule_id, {})
251
+ value = entry.get("calibrated")
252
+ return float(value) if isinstance(value, (int, float)) else None
253
+
254
+ def get_feedback(self, rule_id: str) -> dict[str, int]:
255
+ """Get raw TP/FP counts for a rule."""
256
+ with self._lock:
257
+ entry = self._data.get(rule_id, {})
258
+ return {
259
+ "tp": self._entry_int(entry, "tp"),
260
+ "fp": self._entry_int(entry, "fp"),
261
+ }
262
+
263
+ def all_feedback(self) -> dict[str, dict[str, object]]:
264
+ """Get all stored data (TP/FP + scan/match + disabled state)."""
265
+ with self._lock:
266
+ return dict(self._data)
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Report formatting
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def format_rule_stats(
275
+ stats: dict[str, RuleStats],
276
+ feedback: dict[str, dict[str, object]] | None = None,
277
+ ) -> str:
278
+ """Format profiling stats as markdown."""
279
+ if not stats:
280
+ return "No profiling data available."
281
+
282
+ lines = ["# Rule Profiling Stats\n"]
283
+
284
+ # Sort by total time descending
285
+ sorted_stats = sorted(stats.values(), key=lambda s: s.total_time_ms, reverse=True)
286
+
287
+ lines.append("| Rule | Time (ms) | Matches | Files | TP | FP | Calibrated |")
288
+ lines.append("|------|-----------|---------|-------|----|----|------------|")
289
+
290
+ for s in sorted_stats:
291
+ fb = (feedback or {}).get(s.rule_id, {})
292
+ tp_raw = fb.get("tp", s.true_positives)
293
+ fp_raw = fb.get("fp", s.false_positives)
294
+ tp = int(tp_raw) if isinstance(tp_raw, (int, float)) else s.true_positives
295
+ fp = int(fp_raw) if isinstance(fp_raw, (int, float)) else s.false_positives
296
+ cal_raw = fb.get("calibrated", s.calibrated_confidence)
297
+ cal: float | None
298
+ if isinstance(cal_raw, (int, float)):
299
+ cal = float(cal_raw)
300
+ elif cal_raw is None:
301
+ cal = s.calibrated_confidence
302
+ else:
303
+ cal = None
304
+ cal_str = f"{cal:.2f}" if cal is not None else "—"
305
+ lines.append(
306
+ f"| `{s.rule_id}` | {s.total_time_ms:.1f} | {s.match_count} | "
307
+ f"{s.files_scanned} | {tp} | {fp} | {cal_str} |"
308
+ )
309
+
310
+ # Summary
311
+ total_time = sum(s.total_time_ms for s in sorted_stats)
312
+ total_matches = sum(s.match_count for s in sorted_stats)
313
+ lines.append(f"\n**Total**: {total_time:.1f}ms across {len(sorted_stats)} rules, {total_matches} matches")
314
+
315
+ # Top 5 slowest
316
+ if len(sorted_stats) > 5:
317
+ lines.append("\n## Slowest Rules")
318
+ for s in sorted_stats[:5]:
319
+ lines.append(f"- `{s.rule_id}`: {s.total_time_ms:.1f}ms")
320
+
321
+ # Highest FP rate
322
+ fp_rules = [
323
+ s for s in sorted_stats
324
+ if s.false_positive_rate is not None and s.false_positive_rate > 0
325
+ ]
326
+ if fp_rules:
327
+ fp_rules.sort(key=lambda s: s.false_positive_rate or 0, reverse=True) # type: ignore[arg-type]
328
+ lines.append("\n## Highest False Positive Rates")
329
+ for s in fp_rules[:5]:
330
+ lines.append(f"- `{s.rule_id}`: {s.false_positive_rate:.0%}")
331
+
332
+ return "\n".join(lines)
@@ -0,0 +1,213 @@
1
+ """Rule registry — stores, indexes, and queries UnifiedRule instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import threading
7
+ from collections import defaultdict
8
+
9
+ from attocode_intel.rules.model import (
10
+ RuleCategory,
11
+ RuleSeverity,
12
+ RuleSource,
13
+ RuleTier,
14
+ UnifiedRule,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _SEV_RANK: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
20
+
21
+
22
+ class RuleRegistry:
23
+ """Thread-safe registry of analysis rules.
24
+
25
+ Supports registration from builtins, language packs, and user plugins.
26
+ Provides filtered queries by language, category, severity, pack, etc.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self._lock = threading.Lock()
31
+ self._rules: dict[str, UnifiedRule] = {} # qualified_id -> rule
32
+ self._by_language: dict[str, list[str]] = defaultdict(list)
33
+ self._by_category: dict[RuleCategory, list[str]] = defaultdict(list)
34
+ self._by_pack: dict[str, list[str]] = defaultdict(list)
35
+ self._by_severity: dict[RuleSeverity, list[str]] = defaultdict(list)
36
+
37
+ def register(self, rule: UnifiedRule) -> None:
38
+ """Register a rule. Overwrites if qualified_id already exists."""
39
+ with self._lock:
40
+ qid = rule.qualified_id
41
+ was_update = qid in self._rules
42
+ if was_update:
43
+ self._unindex(qid)
44
+ self._rules[qid] = rule
45
+ self._index(rule)
46
+ if was_update:
47
+ logger.debug("Updated rule %s", qid)
48
+ else:
49
+ logger.debug("Registered rule %s", qid)
50
+
51
+ def register_many(self, rules: list[UnifiedRule]) -> int:
52
+ """Register multiple rules. Returns count registered."""
53
+ for rule in rules:
54
+ self.register(rule)
55
+ return len(rules)
56
+
57
+ def get(self, qualified_id: str) -> UnifiedRule | None:
58
+ with self._lock:
59
+ return self._rules.get(qualified_id)
60
+
61
+ def remove(self, qualified_id: str) -> bool:
62
+ """Remove a rule by qualified ID. Returns True if found."""
63
+ with self._lock:
64
+ if qualified_id not in self._rules:
65
+ return False
66
+ self._unindex(qualified_id)
67
+ del self._rules[qualified_id]
68
+ return True
69
+
70
+ def enable(self, qualified_id: str) -> bool:
71
+ with self._lock:
72
+ rule = self._rules.get(qualified_id)
73
+ if rule is None:
74
+ return False
75
+ rule.enabled = True
76
+ return True
77
+
78
+ def disable(self, qualified_id: str) -> bool:
79
+ with self._lock:
80
+ rule = self._rules.get(qualified_id)
81
+ if rule is None:
82
+ return False
83
+ rule.enabled = False
84
+ return True
85
+
86
+ @property
87
+ def count(self) -> int:
88
+ with self._lock:
89
+ return len(self._rules)
90
+
91
+ def all_rules(self, *, enabled_only: bool = True) -> list[UnifiedRule]:
92
+ with self._lock:
93
+ rules = list(self._rules.values())
94
+ if enabled_only:
95
+ return [r for r in rules if r.enabled]
96
+ return rules
97
+
98
+ def query(
99
+ self,
100
+ *,
101
+ language: str = "",
102
+ category: RuleCategory | str = "",
103
+ severity: RuleSeverity | str = "",
104
+ pack: str = "",
105
+ source: RuleSource | str = "",
106
+ tier: RuleTier | str = "",
107
+ tags: list[str] | None = None,
108
+ enabled_only: bool = True,
109
+ min_confidence: float = 0.0,
110
+ ) -> list[UnifiedRule]:
111
+ """Query rules with optional filters. All filters are AND-combined."""
112
+ # Snapshot under lock
113
+ with self._lock:
114
+ if language:
115
+ candidates = set(self._by_language.get(language, []))
116
+ for qid, rule in self._rules.items():
117
+ if not rule.languages:
118
+ candidates.add(qid)
119
+ else:
120
+ candidates = set(self._rules.keys())
121
+ rules_snapshot = {qid: self._rules[qid] for qid in candidates if qid in self._rules}
122
+
123
+ results: list[UnifiedRule] = []
124
+ for qid, rule in rules_snapshot.items():
125
+ if enabled_only and not rule.enabled:
126
+ continue
127
+ if category and rule.category != category:
128
+ continue
129
+ if severity and _SEV_RANK.get(rule.severity, 9) > _SEV_RANK.get(str(severity), 9):
130
+ continue
131
+ if pack and rule.pack != pack:
132
+ continue
133
+ if source and rule.source != source:
134
+ continue
135
+ if tier and rule.tier != tier:
136
+ continue
137
+ if min_confidence > 0.0 and rule.confidence < min_confidence:
138
+ continue
139
+ if tags and not set(tags).issubset(set(rule.tags)):
140
+ continue
141
+ results.append(rule)
142
+
143
+ _severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
144
+ results.sort(key=lambda r: (_severity_order.get(r.severity, 9), r.qualified_id))
145
+ return results
146
+
147
+ def languages(self) -> list[str]:
148
+ with self._lock:
149
+ return sorted(self._by_language.keys())
150
+
151
+ def packs(self) -> list[str]:
152
+ with self._lock:
153
+ return sorted(self._by_pack.keys())
154
+
155
+ def categories(self) -> list[tuple[RuleCategory, int]]:
156
+ with self._lock:
157
+ return sorted(
158
+ [(cat, len(ids)) for cat, ids in self._by_category.items()],
159
+ key=lambda x: x[1],
160
+ reverse=True,
161
+ )
162
+
163
+ def stats(self) -> dict[str, int | dict[str, int]]:
164
+ with self._lock:
165
+ by_sev = {str(sev): len(ids) for sev, ids in self._by_severity.items()}
166
+ by_cat = {str(cat): len(ids) for cat, ids in self._by_category.items()}
167
+ by_src: dict[str, int] = defaultdict(int)
168
+ by_tier_d: dict[str, int] = defaultdict(int)
169
+ enabled = sum(1 for r in self._rules.values() if r.enabled)
170
+ for r in self._rules.values():
171
+ by_src[str(r.source)] += 1
172
+ by_tier_d[str(r.tier)] += 1
173
+ return {
174
+ "total": len(self._rules),
175
+ "enabled": enabled,
176
+ "by_severity": dict(by_sev),
177
+ "by_category": by_cat,
178
+ "by_source": dict(by_src),
179
+ "by_tier": dict(by_tier_d),
180
+ "languages": len(self._by_language),
181
+ "packs": len(self._by_pack),
182
+ }
183
+
184
+ # --- Internal indexing ---
185
+
186
+ def _index(self, rule: UnifiedRule) -> None:
187
+ qid = rule.qualified_id
188
+ for lang in rule.languages:
189
+ self._by_language[lang].append(qid)
190
+ self._by_category[rule.category].append(qid)
191
+ self._by_severity[rule.severity].append(qid)
192
+ if rule.pack:
193
+ self._by_pack[rule.pack].append(qid)
194
+
195
+ def _unindex(self, qualified_id: str) -> None:
196
+ rule = self._rules.get(qualified_id)
197
+ if rule is None:
198
+ return
199
+ qid = rule.qualified_id
200
+ for lang in rule.languages:
201
+ lst = self._by_language.get(lang, [])
202
+ if qid in lst:
203
+ lst.remove(qid)
204
+ cat_list = self._by_category.get(rule.category, [])
205
+ if qid in cat_list:
206
+ cat_list.remove(qid)
207
+ sev_list = self._by_severity.get(rule.severity, [])
208
+ if qid in sev_list:
209
+ sev_list.remove(qid)
210
+ if rule.pack:
211
+ pack_list = self._by_pack.get(rule.pack, [])
212
+ if qid in pack_list:
213
+ pack_list.remove(qid)