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,972 @@
1
+ """Readiness report engine -- comprehensive codebase audit.
2
+
3
+ Orchestrates existing code-intel tools across 8 phases to produce a
4
+ ground-truth assessment of codebase health, completeness, and readiness.
5
+
6
+ Phases:
7
+ 0. Baseline: build health, git status, dependencies
8
+ 1. API & Business Logic: routes, stubs, TODOs
9
+ 2. Frontend Flows: components, dead-ends
10
+ 3. Data Layer: migrations, models, constraints
11
+ 4. Test Coverage: test-to-source mapping, gaps, skips
12
+ 5. Security Surface: secrets, auth, validation
13
+ 6. Observability: logging, metrics, health endpoints
14
+ 7. Deployability: Docker, CI/CD, env config
15
+
16
+ Plus: Tracer Bullets -- end-to-end journey verification.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import datetime
22
+ import logging
23
+ import os
24
+ import re
25
+ import subprocess
26
+ from dataclasses import dataclass, field
27
+ from enum import IntEnum, StrEnum
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ # =============================================================================
35
+ # Data Model
36
+ # =============================================================================
37
+
38
+
39
+ class ReadinessPhase(IntEnum):
40
+ BASELINE = 0
41
+ API_BUSINESS_LOGIC = 1
42
+ FRONTEND_FLOWS = 2
43
+ DATA_LAYER = 3
44
+ TEST_COVERAGE = 4
45
+ SECURITY_SURFACE = 5
46
+ OBSERVABILITY = 6
47
+ DEPLOYABILITY = 7
48
+
49
+
50
+ _PHASE_NAMES: dict[ReadinessPhase, str] = {
51
+ ReadinessPhase.BASELINE: "Baseline",
52
+ ReadinessPhase.API_BUSINESS_LOGIC: "API & Business Logic",
53
+ ReadinessPhase.FRONTEND_FLOWS: "Frontend Flows",
54
+ ReadinessPhase.DATA_LAYER: "Data Layer",
55
+ ReadinessPhase.TEST_COVERAGE: "Test Coverage",
56
+ ReadinessPhase.SECURITY_SURFACE: "Security Surface",
57
+ ReadinessPhase.OBSERVABILITY: "Observability",
58
+ ReadinessPhase.DEPLOYABILITY: "Deployability",
59
+ }
60
+
61
+
62
+ class ReadinessSeverity(StrEnum):
63
+ PASS = "pass"
64
+ INFO = "info"
65
+ WARNING = "warning"
66
+ CRITICAL = "critical"
67
+
68
+
69
+ @dataclass(slots=True)
70
+ class ReadinessFinding:
71
+ phase: ReadinessPhase
72
+ severity: ReadinessSeverity
73
+ title: str
74
+ detail: str = ""
75
+ evidence: list[str] = field(default_factory=list)
76
+ recommendation: str = ""
77
+
78
+
79
+ @dataclass(slots=True)
80
+ class TracerBulletResult:
81
+ journey_name: str
82
+ status: str # production_ready, happy_path_only, partial, not_implemented
83
+ steps_checked: int
84
+ steps_passed: int
85
+ weak_link: str = ""
86
+ evidence: list[str] = field(default_factory=list)
87
+
88
+
89
+ @dataclass(slots=True)
90
+ class PhaseResult:
91
+ phase: ReadinessPhase
92
+ phase_name: str
93
+ score: float # 0.0-1.0
94
+ findings: list[ReadinessFinding] = field(default_factory=list)
95
+ summary: str = ""
96
+
97
+
98
+ @dataclass(slots=True)
99
+ class ReadinessReport:
100
+ project_name: str
101
+ timestamp: str
102
+ phases_run: list[ReadinessPhase] = field(default_factory=list)
103
+ phase_results: list[PhaseResult] = field(default_factory=list)
104
+ tracer_bullets: list[TracerBulletResult] = field(default_factory=list)
105
+ overall_score: float = 0.0
106
+ overall_grade: str = "F"
107
+
108
+ def to_dict(self) -> dict[str, Any]:
109
+ return {
110
+ "project_name": self.project_name,
111
+ "timestamp": self.timestamp,
112
+ "overall_score": self.overall_score,
113
+ "overall_grade": self.overall_grade,
114
+ "phases": [
115
+ {
116
+ "phase": pr.phase,
117
+ "phase_name": pr.phase_name,
118
+ "score": pr.score,
119
+ "summary": pr.summary,
120
+ "findings": [
121
+ {
122
+ "severity": f.severity,
123
+ "title": f.title,
124
+ "detail": f.detail,
125
+ "evidence": f.evidence,
126
+ "recommendation": f.recommendation,
127
+ }
128
+ for f in pr.findings
129
+ ],
130
+ }
131
+ for pr in self.phase_results
132
+ ],
133
+ "tracer_bullets": [
134
+ {
135
+ "journey_name": tb.journey_name,
136
+ "status": tb.status,
137
+ "steps_checked": tb.steps_checked,
138
+ "steps_passed": tb.steps_passed,
139
+ "weak_link": tb.weak_link,
140
+ "evidence": tb.evidence,
141
+ }
142
+ for tb in self.tracer_bullets
143
+ ],
144
+ }
145
+
146
+
147
+ # =============================================================================
148
+ # Readiness Engine
149
+ # =============================================================================
150
+
151
+
152
+ # Route decorator patterns for different frameworks
153
+ _ROUTE_DECORATOR_RE = re.compile(
154
+ r"@(?:app|router|api|bp|blueprint)\."
155
+ r"(?:route|get|post|put|delete|patch|options|head|websocket)\s*\(",
156
+ re.IGNORECASE,
157
+ )
158
+
159
+ # Stub body patterns
160
+ _STUB_PATTERNS = [
161
+ re.compile(r"^\s*pass\s*$"),
162
+ re.compile(r"^\s*\.\.\.\s*$"),
163
+ re.compile(r"^\s*raise\s+NotImplementedError"),
164
+ ]
165
+
166
+ # Test decorator skip patterns
167
+ _SKIP_PATTERNS = [
168
+ re.compile(r"@pytest\.mark\.skip"),
169
+ re.compile(r"@unittest\.skip"),
170
+ re.compile(r"\.skip\("),
171
+ ]
172
+
173
+ # Framework import patterns for observability
174
+ _LOGGING_IMPORTS = re.compile(r"(?:import\s+logging|from\s+logging\s+import|import\s+structlog|import\s+loguru)")
175
+ _METRICS_IMPORTS = re.compile(r"(?:import\s+prometheus|from\s+prometheus|import\s+statsd|import\s+datadog)")
176
+ _ERROR_TRACKING_IMPORTS = re.compile(r"(?:import\s+sentry_sdk|import\s+bugsnag|import\s+rollbar)")
177
+
178
+ # Auth decorator patterns
179
+ _AUTH_DECORATOR_RE = re.compile(
180
+ r"@(?:login_required|auth_required|requires_auth|authenticated|"
181
+ r"permission_required|jwt_required|token_required|Depends\(.*auth)",
182
+ re.IGNORECASE,
183
+ )
184
+
185
+ # Lock file names
186
+ _LOCK_FILES = {"uv.lock", "poetry.lock", "Pipfile.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "go.sum"}
187
+
188
+ # Frontend file extensions
189
+ _FRONTEND_EXTS = frozenset({".jsx", ".tsx", ".vue", ".svelte"})
190
+
191
+
192
+ class ReadinessEngine:
193
+ """Orchestrates the 8-phase readiness audit."""
194
+
195
+ def __init__(self, project_dir: str) -> None:
196
+ self._project_dir = os.path.abspath(project_dir)
197
+
198
+ def run(
199
+ self,
200
+ *,
201
+ phases: list[int] | None = None,
202
+ scope: str = "",
203
+ tracer_bullets: bool = True,
204
+ prd_text: str = "",
205
+ ) -> ReadinessReport:
206
+ """Run the readiness audit.
207
+
208
+ Args:
209
+ phases: Phase numbers to run (0-7). None = all.
210
+ scope: Restrict to files under this directory prefix.
211
+ tracer_bullets: Whether to run tracer bullet analysis.
212
+ prd_text: Optional PRD text for plan comparison.
213
+
214
+ Returns:
215
+ A ReadinessReport with findings and scores.
216
+ """
217
+ from attocode_intel.helpers import _detect_project_name
218
+
219
+ project_name = _detect_project_name(self._project_dir)
220
+ timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
221
+
222
+ all_phases = list(ReadinessPhase)
223
+ requested = [ReadinessPhase(p) for p in phases] if phases else all_phases
224
+
225
+ phase_methods = {
226
+ ReadinessPhase.BASELINE: self._phase_baseline,
227
+ ReadinessPhase.API_BUSINESS_LOGIC: self._phase_api_logic,
228
+ ReadinessPhase.FRONTEND_FLOWS: self._phase_frontend,
229
+ ReadinessPhase.DATA_LAYER: self._phase_data_layer,
230
+ ReadinessPhase.TEST_COVERAGE: self._phase_test_coverage,
231
+ ReadinessPhase.SECURITY_SURFACE: self._phase_security,
232
+ ReadinessPhase.OBSERVABILITY: self._phase_observability,
233
+ ReadinessPhase.DEPLOYABILITY: self._phase_deployability,
234
+ }
235
+
236
+ results: list[PhaseResult] = []
237
+ for phase in requested:
238
+ method = phase_methods.get(phase)
239
+ if method:
240
+ try:
241
+ result = method(scope)
242
+ results.append(result)
243
+ except Exception as e:
244
+ logger.warning("Phase %s failed: %s", phase, e, exc_info=True)
245
+ results.append(PhaseResult(
246
+ phase=phase,
247
+ phase_name=_PHASE_NAMES[phase],
248
+ score=0.0,
249
+ findings=[ReadinessFinding(
250
+ phase=phase,
251
+ severity=ReadinessSeverity.CRITICAL,
252
+ title=f"Phase {phase.name} failed",
253
+ detail=str(e),
254
+ )],
255
+ summary=f"Phase failed: {e}",
256
+ ))
257
+
258
+ # Tracer bullets
259
+ bullets: list[TracerBulletResult] = []
260
+ if tracer_bullets:
261
+ try:
262
+ bullets = self._run_tracer_bullets(scope)
263
+ except Exception as e:
264
+ logger.warning("Tracer bullets failed: %s", e, exc_info=True)
265
+
266
+ # Compute overall score
267
+ overall_score, overall_grade = self._compute_overall(results)
268
+
269
+ return ReadinessReport(
270
+ project_name=project_name,
271
+ timestamp=timestamp,
272
+ phases_run=requested,
273
+ phase_results=results,
274
+ tracer_bullets=bullets,
275
+ overall_score=overall_score,
276
+ overall_grade=overall_grade,
277
+ )
278
+
279
+ def format_report(self, report: ReadinessReport) -> str:
280
+ """Format a ReadinessReport as human-readable text."""
281
+ lines: list[str] = []
282
+ score_pct = int(report.overall_score * 100)
283
+ lines.append(f"# Readiness Report: {report.project_name}")
284
+ lines.append(f"Generated: {report.timestamp}")
285
+ lines.append(f"Overall Grade: {report.overall_grade} ({score_pct}/100)")
286
+ lines.append("")
287
+
288
+ for pr in report.phase_results:
289
+ phase_pct = int(pr.score * 100)
290
+ lines.append(f"## Phase {pr.phase}: {pr.phase_name} ({phase_pct}/100)")
291
+ if pr.summary:
292
+ lines.append(f" {pr.summary}")
293
+ for f in pr.findings:
294
+ tag = _severity_tag(f.severity)
295
+ lines.append(f" {tag} {f.title}")
296
+ if f.detail:
297
+ lines.append(f" {f.detail}")
298
+ for ev in f.evidence[:5]:
299
+ lines.append(f" {ev}")
300
+ if f.recommendation:
301
+ lines.append(f" -> {f.recommendation}")
302
+ lines.append("")
303
+
304
+ if report.tracer_bullets:
305
+ passing = sum(1 for tb in report.tracer_bullets if tb.status == "production_ready")
306
+ total = len(report.tracer_bullets)
307
+ lines.append(f"## Tracer Bullets ({passing}/{total} passing)")
308
+ for tb in report.tracer_bullets:
309
+ tag = _status_tag(tb.status)
310
+ lines.append(f" {tag} {tb.journey_name} ({tb.steps_passed}/{tb.steps_checked})")
311
+ if tb.weak_link:
312
+ lines.append(f" Weak link: {tb.weak_link}")
313
+ for ev in tb.evidence[:3]:
314
+ lines.append(f" {ev}")
315
+ lines.append("")
316
+
317
+ return "\n".join(lines)
318
+
319
+ # ------------------------------------------------------------------
320
+ # Phase implementations
321
+ # ------------------------------------------------------------------
322
+
323
+ def _phase_baseline(self, scope: str) -> PhaseResult:
324
+ """Phase 0: Can it run?"""
325
+ findings: list[ReadinessFinding] = []
326
+ phase = ReadinessPhase.BASELINE
327
+ score = 1.0
328
+
329
+ # Git status
330
+ try:
331
+ result = subprocess.run(
332
+ ["git", "status", "--porcelain"],
333
+ capture_output=True, text=True, timeout=10,
334
+ cwd=self._project_dir,
335
+ )
336
+ dirty_count = len([l for l in result.stdout.strip().splitlines() if l.strip()])
337
+ if dirty_count == 0:
338
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Git status: clean"))
339
+ else:
340
+ findings.append(ReadinessFinding(
341
+ phase=phase, severity=ReadinessSeverity.WARNING,
342
+ title=f"Git has {dirty_count} uncommitted change(s)",
343
+ recommendation="Commit or stash before deployment",
344
+ ))
345
+ score -= 0.05
346
+ except (subprocess.SubprocessError, FileNotFoundError):
347
+ findings.append(ReadinessFinding(
348
+ phase=phase, severity=ReadinessSeverity.INFO,
349
+ title="Not a git repository or git not available",
350
+ ))
351
+
352
+ # Build system detection
353
+ from attocode_intel.helpers import _detect_build_system
354
+ files = self._get_file_list(scope)
355
+ build_system = _detect_build_system(files)
356
+ if build_system and build_system != "unknown":
357
+ findings.append(ReadinessFinding(
358
+ phase=phase, severity=ReadinessSeverity.PASS,
359
+ title=f"Build system: {build_system}",
360
+ ))
361
+ else:
362
+ findings.append(ReadinessFinding(
363
+ phase=phase, severity=ReadinessSeverity.WARNING,
364
+ title="No build system detected",
365
+ recommendation="Add pyproject.toml, package.json, or equivalent",
366
+ ))
367
+ score -= 0.15
368
+
369
+ # Lock files
370
+ basenames = {os.path.basename(fi.relative_path) for fi in files}
371
+ found_locks = basenames & _LOCK_FILES
372
+ if found_locks:
373
+ findings.append(ReadinessFinding(
374
+ phase=phase, severity=ReadinessSeverity.PASS,
375
+ title=f"Lock file(s): {', '.join(sorted(found_locks))}",
376
+ ))
377
+ else:
378
+ findings.append(ReadinessFinding(
379
+ phase=phase, severity=ReadinessSeverity.WARNING,
380
+ title="No dependency lock file found",
381
+ recommendation="Run `uv lock`, `npm install`, or equivalent to generate a lock file",
382
+ ))
383
+ score -= 0.1
384
+
385
+ # Entry points
386
+ from attocode_intel.helpers import _find_entry_points
387
+ try:
388
+ ast_svc = self._get_ast_service()
389
+ entry_points = _find_entry_points(files, ast_svc.index)
390
+ if entry_points:
391
+ findings.append(ReadinessFinding(
392
+ phase=phase, severity=ReadinessSeverity.PASS,
393
+ title=f"{len(entry_points)} entry point(s) detected",
394
+ evidence=[f"{ep[0]} ({ep[1]})" for ep in entry_points[:5]],
395
+ ))
396
+ else:
397
+ findings.append(ReadinessFinding(
398
+ phase=phase, severity=ReadinessSeverity.INFO,
399
+ title="No clear entry points detected",
400
+ ))
401
+ score -= 0.05
402
+ except Exception:
403
+ pass
404
+
405
+ return PhaseResult(
406
+ phase=phase, phase_name=_PHASE_NAMES[phase],
407
+ score=max(0.0, score),
408
+ findings=findings,
409
+ summary=f"Build: {build_system}, Locks: {', '.join(sorted(found_locks)) or 'none'}",
410
+ )
411
+
412
+ def _phase_api_logic(self, scope: str) -> PhaseResult:
413
+ """Phase 1: Route inventory, stubs, TODOs."""
414
+ findings: list[ReadinessFinding] = []
415
+ phase = ReadinessPhase.API_BUSINESS_LOGIC
416
+ score = 1.0
417
+
418
+ route_handlers: list[tuple[str, int, str]] = [] # (file, line, name)
419
+ stub_functions: list[tuple[str, int, str]] = []
420
+ todo_count = 0
421
+
422
+ for fi in self._get_source_files(scope):
423
+ abs_path = os.path.join(self._project_dir, fi.relative_path)
424
+ try:
425
+ content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
426
+ except OSError:
427
+ continue
428
+
429
+ lines = content.splitlines()
430
+ for i, line in enumerate(lines, 1):
431
+ # Route detection
432
+ if _ROUTE_DECORATOR_RE.search(line):
433
+ # Next non-decorator line is likely the function def
434
+ for j in range(i, min(i + 5, len(lines) + 1)):
435
+ if j - 1 < len(lines) and lines[j - 1].strip().startswith("def "):
436
+ fname = lines[j - 1].strip().split("(")[0].replace("def ", "")
437
+ route_handlers.append((fi.relative_path, j, fname))
438
+ break
439
+
440
+ # TODO/FIXME
441
+ if re.search(r"\b(TODO|FIXME|HACK|XXX)\b", line):
442
+ todo_count += 1
443
+
444
+ # Stub detection in function bodies
445
+ self._detect_stubs(fi.relative_path, lines, stub_functions)
446
+
447
+ if route_handlers:
448
+ findings.append(ReadinessFinding(
449
+ phase=phase, severity=ReadinessSeverity.PASS,
450
+ title=f"{len(route_handlers)} route handler(s) detected",
451
+ evidence=[f"{rh[0]}:{rh[1]} -- {rh[2]}()" for rh in route_handlers[:10]],
452
+ ))
453
+ else:
454
+ findings.append(ReadinessFinding(
455
+ phase=phase, severity=ReadinessSeverity.INFO,
456
+ title="No route handlers detected (may not be a web API)",
457
+ ))
458
+
459
+ if stub_functions:
460
+ findings.append(ReadinessFinding(
461
+ phase=phase, severity=ReadinessSeverity.CRITICAL,
462
+ title=f"{len(stub_functions)} stub function(s) found",
463
+ detail="Functions with only pass, ..., or raise NotImplementedError",
464
+ evidence=[f"{s[0]}:{s[1]} -- {s[2]}()" for s in stub_functions[:10]],
465
+ recommendation="Implement stub functions before shipping",
466
+ ))
467
+ score -= min(0.3, len(stub_functions) * 0.05)
468
+
469
+ if todo_count > 0:
470
+ sev = ReadinessSeverity.WARNING if todo_count > 10 else ReadinessSeverity.INFO
471
+ findings.append(ReadinessFinding(
472
+ phase=phase, severity=sev,
473
+ title=f"{todo_count} TODO/FIXME markers in source code",
474
+ ))
475
+ if todo_count > 20:
476
+ score -= 0.1
477
+
478
+ return PhaseResult(
479
+ phase=phase, phase_name=_PHASE_NAMES[phase],
480
+ score=max(0.0, score),
481
+ findings=findings,
482
+ summary=f"{len(route_handlers)} routes, {len(stub_functions)} stubs, {todo_count} TODOs",
483
+ )
484
+
485
+ def _phase_frontend(self, scope: str) -> PhaseResult:
486
+ """Phase 2: Frontend component inventory."""
487
+ findings: list[ReadinessFinding] = []
488
+ phase = ReadinessPhase.FRONTEND_FLOWS
489
+
490
+ frontend_files = [
491
+ fi for fi in self._get_source_files(scope)
492
+ if Path(fi.relative_path).suffix.lower() in _FRONTEND_EXTS
493
+ ]
494
+
495
+ if not frontend_files:
496
+ findings.append(ReadinessFinding(
497
+ phase=phase, severity=ReadinessSeverity.PASS,
498
+ title="No frontend files detected (not a frontend project)",
499
+ ))
500
+ return PhaseResult(
501
+ phase=phase, phase_name=_PHASE_NAMES[phase],
502
+ score=1.0, findings=findings,
503
+ summary="Skipped (no frontend files)",
504
+ )
505
+
506
+ findings.append(ReadinessFinding(
507
+ phase=phase, severity=ReadinessSeverity.PASS,
508
+ title=f"{len(frontend_files)} frontend component file(s)",
509
+ evidence=[fi.relative_path for fi in frontend_files[:10]],
510
+ ))
511
+
512
+ return PhaseResult(
513
+ phase=phase, phase_name=_PHASE_NAMES[phase],
514
+ score=0.8,
515
+ findings=findings,
516
+ summary=f"{len(frontend_files)} frontend files",
517
+ )
518
+
519
+ def _phase_data_layer(self, scope: str) -> PhaseResult:
520
+ """Phase 3: Migrations, models, constraints."""
521
+ findings: list[ReadinessFinding] = []
522
+ phase = ReadinessPhase.DATA_LAYER
523
+ score = 1.0
524
+
525
+ # Migration detection
526
+ migration_dirs: list[str] = []
527
+ for dirpath, dirnames, filenames in os.walk(self._project_dir):
528
+ rel = os.path.relpath(dirpath, self._project_dir)
529
+ basename = os.path.basename(dirpath)
530
+ if basename in ("migrations", "alembic", "prisma", "versions"):
531
+ migration_dirs.append(rel)
532
+ # Don't recurse into common skip dirs
533
+ dirnames[:] = [d for d in dirnames if d not in {".git", "node_modules", "__pycache__", ".venv"}]
534
+
535
+ if migration_dirs:
536
+ findings.append(ReadinessFinding(
537
+ phase=phase, severity=ReadinessSeverity.PASS,
538
+ title=f"Migration directory found: {', '.join(migration_dirs[:3])}",
539
+ ))
540
+ else:
541
+ findings.append(ReadinessFinding(
542
+ phase=phase, severity=ReadinessSeverity.INFO,
543
+ title="No migration directories found",
544
+ detail="May not be a database-backed project",
545
+ ))
546
+
547
+ # ORM model detection
548
+ model_count = 0
549
+ for fi in self._get_source_files(scope):
550
+ abs_path = os.path.join(self._project_dir, fi.relative_path)
551
+ try:
552
+ content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
553
+ except OSError:
554
+ continue
555
+ # SQLAlchemy/Django model patterns
556
+ if re.search(r"class\s+\w+\(.*(?:Base|Model|db\.Model|DeclarativeBase)", content):
557
+ model_count += 1
558
+
559
+ if model_count:
560
+ findings.append(ReadinessFinding(
561
+ phase=phase, severity=ReadinessSeverity.PASS,
562
+ title=f"{model_count} ORM model file(s) detected",
563
+ ))
564
+
565
+ return PhaseResult(
566
+ phase=phase, phase_name=_PHASE_NAMES[phase],
567
+ score=max(0.0, score),
568
+ findings=findings,
569
+ summary=f"Migrations: {len(migration_dirs)}, Models: {model_count}",
570
+ )
571
+
572
+ def _phase_test_coverage(self, scope: str) -> PhaseResult:
573
+ """Phase 4: Test file inventory, coverage gaps, skipped tests."""
574
+ findings: list[ReadinessFinding] = []
575
+ phase = ReadinessPhase.TEST_COVERAGE
576
+ score = 1.0
577
+
578
+ all_files = self._get_file_list(scope)
579
+ test_files = [fi for fi in all_files if fi.is_test]
580
+ source_files = [fi for fi in all_files if not fi.is_test and not fi.is_config]
581
+
582
+ if not source_files:
583
+ return PhaseResult(
584
+ phase=phase, phase_name=_PHASE_NAMES[phase],
585
+ score=1.0, findings=[],
586
+ summary="No source files to test",
587
+ )
588
+
589
+ test_ratio = len(test_files) / max(len(source_files), 1)
590
+
591
+ findings.append(ReadinessFinding(
592
+ phase=phase, severity=ReadinessSeverity.PASS if test_ratio >= 0.3 else ReadinessSeverity.WARNING,
593
+ title=f"{len(test_files)} test file(s) for {len(source_files)} source file(s) ({test_ratio:.0%})",
594
+ ))
595
+
596
+ if test_ratio < 0.1:
597
+ score -= 0.4
598
+ elif test_ratio < 0.3:
599
+ score -= 0.2
600
+
601
+ # Detect skipped tests
602
+ skip_count = 0
603
+ for fi in test_files:
604
+ abs_path = os.path.join(self._project_dir, fi.relative_path)
605
+ try:
606
+ content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
607
+ except OSError:
608
+ continue
609
+ for pat in _SKIP_PATTERNS:
610
+ skip_count += len(pat.findall(content))
611
+
612
+ if skip_count > 0:
613
+ findings.append(ReadinessFinding(
614
+ phase=phase, severity=ReadinessSeverity.WARNING,
615
+ title=f"{skip_count} skipped test(s) detected",
616
+ recommendation="Review and either fix or remove skipped tests",
617
+ ))
618
+ score -= min(0.15, skip_count * 0.02)
619
+
620
+ return PhaseResult(
621
+ phase=phase, phase_name=_PHASE_NAMES[phase],
622
+ score=max(0.0, score),
623
+ findings=findings,
624
+ summary=f"{len(test_files)} tests / {len(source_files)} source ({test_ratio:.0%}), {skip_count} skipped",
625
+ )
626
+
627
+ def _phase_security(self, scope: str) -> PhaseResult:
628
+ """Phase 5: Security scan + auth checks."""
629
+ findings: list[ReadinessFinding] = []
630
+ phase = ReadinessPhase.SECURITY_SURFACE
631
+ score = 1.0
632
+
633
+ # Use existing SecurityScanner
634
+ try:
635
+ from attocode_intel._internal.integrations.security.scanner import SecurityScanner
636
+ scanner = SecurityScanner(root_dir=self._project_dir)
637
+ report = scanner.scan(mode="full", path=scope)
638
+ compliance = report.compliance_score / 100.0 if hasattr(report, "compliance_score") else 0.8
639
+
640
+ critical_findings = [f for f in report.findings if getattr(f, "severity", "") == "high"]
641
+ if critical_findings:
642
+ findings.append(ReadinessFinding(
643
+ phase=phase, severity=ReadinessSeverity.CRITICAL,
644
+ title=f"{len(critical_findings)} high-severity security finding(s)",
645
+ evidence=[str(f)[:100] for f in critical_findings[:5]],
646
+ ))
647
+ score -= min(0.4, len(critical_findings) * 0.1)
648
+ else:
649
+ findings.append(ReadinessFinding(
650
+ phase=phase, severity=ReadinessSeverity.PASS,
651
+ title="No high-severity security findings",
652
+ ))
653
+
654
+ findings.append(ReadinessFinding(
655
+ phase=phase, severity=ReadinessSeverity.PASS if compliance >= 0.8 else ReadinessSeverity.WARNING,
656
+ title=f"Security compliance score: {compliance:.0%}",
657
+ ))
658
+ score = min(score, compliance)
659
+ except Exception as e:
660
+ findings.append(ReadinessFinding(
661
+ phase=phase, severity=ReadinessSeverity.INFO,
662
+ title=f"Security scanner unavailable: {e}",
663
+ ))
664
+
665
+ return PhaseResult(
666
+ phase=phase, phase_name=_PHASE_NAMES[phase],
667
+ score=max(0.0, score),
668
+ findings=findings,
669
+ )
670
+
671
+ def _phase_observability(self, scope: str) -> PhaseResult:
672
+ """Phase 6: Logging, metrics, error tracking."""
673
+ findings: list[ReadinessFinding] = []
674
+ phase = ReadinessPhase.OBSERVABILITY
675
+ score = 0.5 # Start lower, add for each capability found
676
+
677
+ has_logging = False
678
+ has_metrics = False
679
+ has_error_tracking = False
680
+
681
+ for fi in self._get_source_files(scope):
682
+ abs_path = os.path.join(self._project_dir, fi.relative_path)
683
+ try:
684
+ content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
685
+ except OSError:
686
+ continue
687
+ if _LOGGING_IMPORTS.search(content):
688
+ has_logging = True
689
+ if _METRICS_IMPORTS.search(content):
690
+ has_metrics = True
691
+ if _ERROR_TRACKING_IMPORTS.search(content):
692
+ has_error_tracking = True
693
+
694
+ if has_logging:
695
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Logging framework detected"))
696
+ score += 0.2
697
+ else:
698
+ findings.append(ReadinessFinding(
699
+ phase=phase, severity=ReadinessSeverity.WARNING, title="No logging framework detected",
700
+ recommendation="Add structured logging (logging, structlog, or loguru)",
701
+ ))
702
+
703
+ if has_metrics:
704
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Metrics framework detected"))
705
+ score += 0.15
706
+ else:
707
+ findings.append(ReadinessFinding(
708
+ phase=phase, severity=ReadinessSeverity.INFO, title="No metrics framework detected",
709
+ ))
710
+
711
+ if has_error_tracking:
712
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Error tracking detected"))
713
+ score += 0.15
714
+ else:
715
+ findings.append(ReadinessFinding(
716
+ phase=phase, severity=ReadinessSeverity.INFO, title="No error tracking detected",
717
+ ))
718
+
719
+ return PhaseResult(
720
+ phase=phase, phase_name=_PHASE_NAMES[phase],
721
+ score=min(1.0, score),
722
+ findings=findings,
723
+ summary=f"Logging: {'yes' if has_logging else 'no'}, Metrics: {'yes' if has_metrics else 'no'}, Errors: {'yes' if has_error_tracking else 'no'}",
724
+ )
725
+
726
+ def _phase_deployability(self, scope: str) -> PhaseResult:
727
+ """Phase 7: Docker, CI/CD, env config."""
728
+ findings: list[ReadinessFinding] = []
729
+ phase = ReadinessPhase.DEPLOYABILITY
730
+ score = 0.5
731
+
732
+ pd = self._project_dir
733
+
734
+ # Dockerfile
735
+ if os.path.isfile(os.path.join(pd, "Dockerfile")):
736
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Dockerfile present"))
737
+ score += 0.15
738
+ elif os.path.isfile(os.path.join(pd, "docker-compose.yml")) or os.path.isfile(os.path.join(pd, "docker-compose.yaml")):
739
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="docker-compose config present"))
740
+ score += 0.1
741
+ else:
742
+ findings.append(ReadinessFinding(
743
+ phase=phase, severity=ReadinessSeverity.INFO, title="No Dockerfile found",
744
+ ))
745
+
746
+ # CI/CD
747
+ ci_found = False
748
+ if os.path.isdir(os.path.join(pd, ".github", "workflows")):
749
+ ci_found = True
750
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="GitHub Actions workflows found"))
751
+ if os.path.isfile(os.path.join(pd, ".gitlab-ci.yml")):
752
+ ci_found = True
753
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="GitLab CI config found"))
754
+ if os.path.isfile(os.path.join(pd, "Jenkinsfile")):
755
+ ci_found = True
756
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Jenkinsfile found"))
757
+
758
+ if ci_found:
759
+ score += 0.2
760
+ else:
761
+ findings.append(ReadinessFinding(
762
+ phase=phase, severity=ReadinessSeverity.WARNING, title="No CI/CD configuration found",
763
+ recommendation="Add .github/workflows/, .gitlab-ci.yml, or equivalent",
764
+ ))
765
+
766
+ # .env.example
767
+ if os.path.isfile(os.path.join(pd, ".env.example")):
768
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title=".env.example present"))
769
+ score += 0.1
770
+ elif os.path.isfile(os.path.join(pd, ".env.template")):
771
+ findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title=".env.template present"))
772
+ score += 0.1
773
+
774
+ return PhaseResult(
775
+ phase=phase, phase_name=_PHASE_NAMES[phase],
776
+ score=min(1.0, score),
777
+ findings=findings,
778
+ )
779
+
780
+ # ------------------------------------------------------------------
781
+ # Tracer Bullets
782
+ # ------------------------------------------------------------------
783
+
784
+ def _run_tracer_bullets(self, scope: str) -> list[TracerBulletResult]:
785
+ """Trace top entry point -> dependency chains for end-to-end verification."""
786
+ results: list[TracerBulletResult] = []
787
+ try:
788
+ ast_svc = self._get_ast_service()
789
+ except Exception:
790
+ return results
791
+
792
+ # Find entry-like files (high importance or entry filenames)
793
+ from attocode_intel.helpers import _find_entry_points
794
+ files = self._get_file_list(scope)
795
+ entry_points = _find_entry_points(files, ast_svc.index)
796
+
797
+ for ep_path, ep_reason in entry_points[:10]:
798
+ # Trace dependency chain
799
+ deps = list(ast_svc.index.file_dependencies.get(ep_path, set()))[:10]
800
+ steps_total = 1 + len(deps)
801
+ steps_passed = 1 # Entry point exists
802
+
803
+ weak_link = ""
804
+ evidence = [f"Entry: {ep_path} ({ep_reason})"]
805
+
806
+ for dep in deps:
807
+ dep_abs = os.path.join(self._project_dir, dep)
808
+ if os.path.isfile(dep_abs):
809
+ steps_passed += 1
810
+ else:
811
+ if not weak_link:
812
+ weak_link = f"Missing dependency: {dep}"
813
+
814
+ ratio = steps_passed / steps_total if steps_total > 0 else 0
815
+ if ratio >= 1.0:
816
+ status = "production_ready"
817
+ elif ratio >= 0.7:
818
+ status = "happy_path_only"
819
+ elif ratio > 0:
820
+ status = "partial"
821
+ else:
822
+ status = "not_implemented"
823
+
824
+ results.append(TracerBulletResult(
825
+ journey_name=ep_path,
826
+ status=status,
827
+ steps_checked=steps_total,
828
+ steps_passed=steps_passed,
829
+ weak_link=weak_link,
830
+ evidence=evidence,
831
+ ))
832
+
833
+ return results
834
+
835
+ # ------------------------------------------------------------------
836
+ # Scoring
837
+ # ------------------------------------------------------------------
838
+
839
+ @staticmethod
840
+ def _compute_overall(results: list[PhaseResult]) -> tuple[float, str]:
841
+ """Compute weighted overall score and letter grade."""
842
+ if not results:
843
+ return 0.0, "F"
844
+
845
+ weights = {
846
+ ReadinessPhase.BASELINE: 2.0,
847
+ ReadinessPhase.TEST_COVERAGE: 2.0,
848
+ ReadinessPhase.SECURITY_SURFACE: 2.0,
849
+ }
850
+ total_weight = 0.0
851
+ weighted_sum = 0.0
852
+ for pr in results:
853
+ w = weights.get(pr.phase, 1.0)
854
+ weighted_sum += pr.score * w
855
+ total_weight += w
856
+
857
+ score = weighted_sum / total_weight if total_weight > 0 else 0.0
858
+
859
+ if score >= 0.9:
860
+ grade = "A"
861
+ elif score >= 0.75:
862
+ grade = "B"
863
+ elif score >= 0.6:
864
+ grade = "C"
865
+ elif score >= 0.45:
866
+ grade = "D"
867
+ else:
868
+ grade = "F"
869
+
870
+ return round(score, 3), grade
871
+
872
+ # ------------------------------------------------------------------
873
+ # Helpers
874
+ # ------------------------------------------------------------------
875
+
876
+ def _get_ast_service(self):
877
+ """Get the AST service for the project."""
878
+ from attocode_intel._internal.integrations.context.ast_service import ASTService
879
+ svc = ASTService.get_instance(self._project_dir)
880
+ if not svc.initialized:
881
+ svc.initialize()
882
+ return svc
883
+
884
+ def _get_file_list(self, scope: str) -> list:
885
+ """Get the file list from CodebaseContextManager."""
886
+ from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
887
+ ctx = CodebaseContextManager(root_dir=self._project_dir)
888
+ ctx.discover_files()
889
+ files = ctx._files
890
+ if scope:
891
+ files = [fi for fi in files if fi.relative_path.startswith(scope)]
892
+ return files
893
+
894
+ def _get_source_files(self, scope: str) -> list:
895
+ """Get non-test, non-config source files."""
896
+ return [
897
+ fi for fi in self._get_file_list(scope)
898
+ if not fi.is_test and not fi.is_config
899
+ ]
900
+
901
+ @staticmethod
902
+ def _detect_stubs(
903
+ rel_path: str,
904
+ lines: list[str],
905
+ stubs: list[tuple[str, int, str]],
906
+ ) -> None:
907
+ """Detect stub functions in source code."""
908
+ i = 0
909
+ while i < len(lines):
910
+ line = lines[i]
911
+ stripped = line.strip()
912
+
913
+ # Look for function definitions
914
+ if stripped.startswith("def ") and "(" in stripped:
915
+ fname = stripped.split("(")[0].replace("def ", "").strip()
916
+ # Skip dunder methods and abstract methods
917
+ if fname.startswith("__") and fname.endswith("__"):
918
+ i += 1
919
+ continue
920
+
921
+ # Check the function body (next non-empty, non-comment line)
922
+ body_start = i + 1
923
+ while body_start < len(lines):
924
+ body_line = lines[body_start].strip()
925
+ if body_line and not body_line.startswith("#") and not body_line.startswith('"""') and not body_line.startswith("'''"):
926
+ break
927
+ # Skip docstrings
928
+ if body_line.startswith('"""') or body_line.startswith("'''"):
929
+ quote = body_line[:3]
930
+ if body_line.count(quote) >= 2:
931
+ body_start += 1
932
+ else:
933
+ body_start += 1
934
+ while body_start < len(lines) and quote not in lines[body_start]:
935
+ body_start += 1
936
+ body_start += 1
937
+ continue
938
+ body_start += 1
939
+
940
+ if body_start < len(lines):
941
+ body_line = lines[body_start].strip()
942
+ for pat in _STUB_PATTERNS:
943
+ if pat.match(body_line):
944
+ stubs.append((rel_path, i + 1, fname))
945
+ break
946
+
947
+ i += 1
948
+
949
+
950
+ # =============================================================================
951
+ # Formatting helpers
952
+ # =============================================================================
953
+
954
+
955
+ def _severity_tag(severity: ReadinessSeverity) -> str:
956
+ tags = {
957
+ ReadinessSeverity.PASS: "[PASS]",
958
+ ReadinessSeverity.INFO: "[INFO]",
959
+ ReadinessSeverity.WARNING: "[WARN]",
960
+ ReadinessSeverity.CRITICAL: "[CRIT]",
961
+ }
962
+ return tags.get(severity, "[????]")
963
+
964
+
965
+ def _status_tag(status: str) -> str:
966
+ tags = {
967
+ "production_ready": "[PASS]",
968
+ "happy_path_only": "[WARN]",
969
+ "partial": "[PART]",
970
+ "not_implemented": "[FAIL]",
971
+ }
972
+ return tags.get(status, "[????]")