alysis-code 0.13.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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,643 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path, PurePosixPath
6
+ from typing import Any
7
+
8
+ from ..repo_scan import scan_workspace
9
+ from ..workspace_context import resolve_workspace_context
10
+
11
+
12
+ class TestDiscoveryError(RuntimeError):
13
+ pass
14
+
15
+
16
+ _PY_EXTENSIONS = {".py"}
17
+ _JS_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
18
+ _SOURCE_PREFIXES = ("src/", "lib/", "app/", "packages/")
19
+ _MAX_TEST_FILE_SCAN = 600
20
+ _GIT_TIMEOUT_S = 2.0
21
+
22
+
23
+ def test_discover(
24
+ *,
25
+ root: Path,
26
+ paths: list[str] | None = None,
27
+ symbols: list[str] | None = None,
28
+ changed_only: bool = False,
29
+ include_commands: bool = True,
30
+ max_results: int = 20,
31
+ failure_summary: dict[str, Any] | None = None,
32
+ ) -> dict[str, Any]:
33
+ root_abs = root.resolve()
34
+ safe_max = max(1, min(int(max_results), 100))
35
+ normalized_paths = _normalized_input_paths(root=root_abs, paths=paths or [])
36
+ if changed_only and not normalized_paths:
37
+ normalized_paths = _git_changed_paths(root_abs)
38
+
39
+ summary_paths, summary_tests = _paths_from_failure_summary(
40
+ root=root_abs,
41
+ failure_summary=failure_summary,
42
+ )
43
+ normalized_paths = _dedupe([*normalized_paths, *summary_paths])
44
+
45
+ candidate_tests: list[dict[str, Any]] = []
46
+ candidate_commands: list[dict[str, Any]] = []
47
+ frameworks: set[str] = set()
48
+
49
+ for test in summary_tests:
50
+ candidate_tests.append(test)
51
+ path = str(test.get("path") or "")
52
+ if path.endswith(".py"):
53
+ frameworks.add("pytest")
54
+ if include_commands:
55
+ node_id = str(test.get("id") or path)
56
+ candidate_commands.append(
57
+ _command(
58
+ f"python -m pytest {node_id} -q",
59
+ scope="targeted",
60
+ confidence=0.92,
61
+ reason="failing pytest nodeid reported by verification output",
62
+ )
63
+ )
64
+
65
+ test_files = _iter_test_files(root_abs)
66
+ for rel_path in normalized_paths:
67
+ suffix = PurePosixPath(rel_path).suffix.lower()
68
+ if suffix in _PY_EXTENSIONS:
69
+ frameworks.add("pytest")
70
+ candidate_tests.extend(_python_test_candidates(root_abs, rel_path, test_files))
71
+ elif suffix in _JS_EXTENSIONS:
72
+ frameworks.update(_node_frameworks(root_abs))
73
+ candidate_tests.extend(_node_test_candidates(root_abs, rel_path, test_files))
74
+ elif suffix == ".go":
75
+ frameworks.add("go test")
76
+ if include_commands:
77
+ candidate_commands.append(_go_test_command(rel_path))
78
+ elif suffix == ".rs":
79
+ frameworks.add("cargo test")
80
+ if include_commands:
81
+ candidate_commands.append(
82
+ _command(
83
+ "cargo test", scope="broad", confidence=0.55, reason="Rust source changed"
84
+ )
85
+ )
86
+ elif suffix == ".java":
87
+ frameworks.add("junit")
88
+ candidate_tests.extend(_java_test_candidates(rel_path, test_files))
89
+
90
+ if symbols:
91
+ candidate_tests.extend(
92
+ _symbol_named_test_candidates(symbols=symbols, test_files=test_files)
93
+ )
94
+
95
+ candidate_tests = _dedupe_test_candidates(candidate_tests)[:safe_max]
96
+ if include_commands:
97
+ candidate_commands.extend(
98
+ _commands_for_tests(root_abs, candidate_tests, frameworks=frameworks)
99
+ )
100
+ if not candidate_commands:
101
+ candidate_commands.extend(_broad_commands(root_abs))
102
+ candidate_commands = _dedupe_commands(candidate_commands)[:safe_max]
103
+
104
+ broad_commands = _broad_commands(root_abs)
105
+ if not frameworks:
106
+ frameworks.update(_frameworks_from_commands(broad_commands))
107
+
108
+ return {
109
+ "paths": normalized_paths,
110
+ "symbols": [str(symbol) for symbol in (symbols or []) if str(symbol).strip()],
111
+ "frameworks": sorted(frameworks),
112
+ "candidate_tests": candidate_tests,
113
+ "candidate_commands": candidate_commands if include_commands else [],
114
+ "broad_commands": [item["command"] for item in broad_commands] if include_commands else [],
115
+ "changed_only": bool(changed_only),
116
+ "heuristic": True,
117
+ }
118
+
119
+
120
+ def _normalized_input_paths(*, root: Path, paths: list[str]) -> list[str]:
121
+ normalized: list[str] = []
122
+ for raw in paths:
123
+ value = _normalize_repo_relative_path(root=root, raw_path=str(raw or ""))
124
+ if value:
125
+ normalized.append(value)
126
+ return _dedupe(normalized)
127
+
128
+
129
+ def _normalize_repo_relative_path(*, root: Path, raw_path: str) -> str:
130
+ value = str(raw_path or "").strip().strip("'\"")
131
+ if not value:
132
+ return ""
133
+ value = value.replace("\\", "/")
134
+ candidate = Path(value)
135
+ try:
136
+ resolved = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
137
+ relative = resolved.relative_to(root).as_posix()
138
+ except (OSError, ValueError):
139
+ return ""
140
+ return "" if relative == "." else relative
141
+
142
+
143
+ def _normalize_pytest_nodeid(*, root: Path, raw_nodeid: str) -> str:
144
+ nodeid = str(raw_nodeid or "").strip()
145
+ if not nodeid:
146
+ return ""
147
+ path_part, sep, suffix = nodeid.partition("::")
148
+ if not path_part.endswith(".py"):
149
+ normalized_prefix = path_part.replace("\\", "/")
150
+ path_parts = {part for part in normalized_prefix.split("/") if part}
151
+ if sep and ("/" in normalized_prefix or ".." in path_parts):
152
+ return ""
153
+ return nodeid
154
+ normalized_path = _normalize_repo_relative_path(root=root, raw_path=path_part)
155
+ if not normalized_path:
156
+ return ""
157
+ return f"{normalized_path}{sep}{suffix}" if sep else normalized_path
158
+
159
+
160
+ def _path_from_normalized_pytest_nodeid(nodeid: str) -> str:
161
+ path_part, sep, _suffix = str(nodeid or "").partition("::")
162
+ if sep and path_part.endswith(".py"):
163
+ return path_part
164
+ return ""
165
+
166
+
167
+ def _git_changed_paths(root: Path) -> list[str]:
168
+ try:
169
+ cp = subprocess.run(
170
+ ["git", "diff", "--name-only", "HEAD", "--"],
171
+ cwd=root,
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=_GIT_TIMEOUT_S,
175
+ check=False,
176
+ )
177
+ except (OSError, subprocess.TimeoutExpired):
178
+ return []
179
+ if cp.returncode != 0:
180
+ return []
181
+ return _dedupe([line.strip() for line in cp.stdout.splitlines() if line.strip()])
182
+
183
+
184
+ def _paths_from_failure_summary(
185
+ *,
186
+ root: Path,
187
+ failure_summary: dict[str, Any] | None,
188
+ ) -> tuple[list[str], list[dict[str, Any]]]:
189
+ if not isinstance(failure_summary, dict):
190
+ return ([], [])
191
+ paths: list[str] = []
192
+ tests: list[dict[str, Any]] = []
193
+ raw_tests = failure_summary.get("failing_tests")
194
+ if isinstance(raw_tests, list):
195
+ for raw in raw_tests:
196
+ if not isinstance(raw, dict):
197
+ continue
198
+ path = _normalize_repo_relative_path(root=root, raw_path=str(raw.get("path") or ""))
199
+ test_id = _normalize_pytest_nodeid(root=root, raw_nodeid=str(raw.get("id") or ""))
200
+ if not path:
201
+ path = _path_from_normalized_pytest_nodeid(test_id)
202
+ if path:
203
+ paths.append(path)
204
+ if not path and not test_id:
205
+ continue
206
+ item: dict[str, Any] = {
207
+ "path": path,
208
+ "confidence": 0.95,
209
+ "reason": "failing test reported by verification output",
210
+ }
211
+ if test_id:
212
+ item["id"] = test_id
213
+ if raw.get("line") is not None:
214
+ item["line"] = raw.get("line")
215
+ message = str(raw.get("message") or "").strip()
216
+ if message:
217
+ item["message"] = message[:240]
218
+ if path or test_id:
219
+ tests.append(item)
220
+ for key in ("likely_next_files", "stack_frames"):
221
+ raw_items = failure_summary.get(key)
222
+ if not isinstance(raw_items, list):
223
+ continue
224
+ for raw in raw_items:
225
+ if isinstance(raw, dict):
226
+ path = _normalize_repo_relative_path(root=root, raw_path=str(raw.get("path") or ""))
227
+ else:
228
+ path = _normalize_repo_relative_path(root=root, raw_path=str(raw or ""))
229
+ if path:
230
+ paths.append(path)
231
+ return (_dedupe(paths), tests)
232
+
233
+
234
+ def _iter_test_files(root: Path) -> list[str]:
235
+ files: list[str] = []
236
+ for path in root.rglob("*"):
237
+ if len(files) >= _MAX_TEST_FILE_SCAN:
238
+ break
239
+ if not path.is_file():
240
+ continue
241
+ rel = path.relative_to(root).as_posix()
242
+ if _is_ignored(rel):
243
+ continue
244
+ name = path.name.lower()
245
+ suffix = path.suffix.lower()
246
+ if (
247
+ rel.startswith(("tests/", "test/"))
248
+ or "__tests__/" in rel
249
+ or name.startswith("test_")
250
+ or name.endswith("_test.py")
251
+ or ".test." in name
252
+ or ".spec." in name
253
+ or name.endswith("test.go")
254
+ or name.endswith("_test.rs")
255
+ or name.endswith("test.java")
256
+ ) and suffix in _PY_EXTENSIONS | _JS_EXTENSIONS | {".go", ".rs", ".java"}:
257
+ files.append(rel)
258
+ return sorted(files)
259
+
260
+
261
+ def _python_test_candidates(
262
+ root: Path, rel_path: str, test_files: list[str]
263
+ ) -> list[dict[str, Any]]:
264
+ pure = PurePosixPath(rel_path)
265
+ stem = pure.stem
266
+ module = _python_module_name(rel_path)
267
+ candidates: list[dict[str, Any]] = []
268
+ for test_file in test_files:
269
+ name = PurePosixPath(test_file).name
270
+ score = 0.0
271
+ reasons: list[str] = []
272
+ if test_file == rel_path:
273
+ score = 0.98
274
+ reasons.append("input path is already a Python test file")
275
+ if name in {f"test_{stem}.py", f"{stem}_test.py"}:
276
+ score = max(score, 0.86)
277
+ reasons.append("test filename mirrors source filename")
278
+ if f"/test_{stem}.py" in test_file or f"/{stem}_test.py" in test_file:
279
+ score = max(score, 0.78)
280
+ reasons.append("test path mirrors source filename")
281
+ if module and _file_contains(root / test_file, module):
282
+ score = max(score, 0.9)
283
+ reasons.append(f"test imports or references {module}")
284
+ if score:
285
+ candidates.append(
286
+ {
287
+ "path": test_file,
288
+ "confidence": round(score, 2),
289
+ "reason": "; ".join(reasons),
290
+ }
291
+ )
292
+ return candidates
293
+
294
+
295
+ def _node_test_candidates(root: Path, rel_path: str, test_files: list[str]) -> list[dict[str, Any]]:
296
+ stem = PurePosixPath(rel_path).stem
297
+ candidates: list[dict[str, Any]] = []
298
+ for test_file in test_files:
299
+ name = PurePosixPath(test_file).name.lower()
300
+ score = 0.0
301
+ reasons: list[str] = []
302
+ if test_file == rel_path:
303
+ score = 0.98
304
+ reasons.append("input path is already a JS/TS test file")
305
+ if name.startswith(stem.lower() + ".") and (".test." in name or ".spec." in name):
306
+ score = max(score, 0.84)
307
+ reasons.append("test filename mirrors source filename")
308
+ if _file_contains(root / test_file, PurePosixPath(rel_path).stem):
309
+ score = max(score, 0.68)
310
+ reasons.append("test references source basename")
311
+ if score:
312
+ candidates.append(
313
+ {
314
+ "path": test_file,
315
+ "confidence": round(score, 2),
316
+ "reason": "; ".join(reasons),
317
+ }
318
+ )
319
+ return candidates
320
+
321
+
322
+ def _java_test_candidates(rel_path: str, test_files: list[str]) -> list[dict[str, Any]]:
323
+ stem = PurePosixPath(rel_path).stem
324
+ return [
325
+ {
326
+ "path": test_file,
327
+ "confidence": 0.72,
328
+ "reason": "Java test class mirrors source class name",
329
+ }
330
+ for test_file in test_files
331
+ if PurePosixPath(test_file).name == f"{stem}Test.java"
332
+ ]
333
+
334
+
335
+ def _symbol_named_test_candidates(
336
+ *,
337
+ symbols: list[str],
338
+ test_files: list[str],
339
+ ) -> list[dict[str, Any]]:
340
+ candidates: list[dict[str, Any]] = []
341
+ lowered_symbols = [str(symbol).strip().casefold() for symbol in symbols if str(symbol).strip()]
342
+ for test_file in test_files:
343
+ test_name = PurePosixPath(test_file).stem.casefold()
344
+ for symbol in lowered_symbols:
345
+ token = symbol.split(".")[-1]
346
+ if token and token in test_name:
347
+ candidates.append(
348
+ {
349
+ "path": test_file,
350
+ "confidence": 0.62,
351
+ "reason": f"test filename mentions symbol {symbol}",
352
+ }
353
+ )
354
+ return candidates
355
+
356
+
357
+ def _commands_for_tests(
358
+ root: Path,
359
+ candidate_tests: list[dict[str, Any]],
360
+ *,
361
+ frameworks: set[str],
362
+ ) -> list[dict[str, Any]]:
363
+ commands: list[dict[str, Any]] = []
364
+ for candidate in candidate_tests:
365
+ path = str(candidate.get("path") or "").strip()
366
+ if not path:
367
+ continue
368
+ suffix = PurePosixPath(path).suffix.lower()
369
+ confidence = float(candidate.get("confidence") or 0.5)
370
+ if suffix == ".py":
371
+ commands.append(
372
+ _command(
373
+ f"python -m pytest {path} -q",
374
+ scope="targeted",
375
+ confidence=min(0.9, max(confidence, 0.55)),
376
+ reason=f"pytest candidate for {path}",
377
+ )
378
+ )
379
+ elif suffix in _JS_EXTENSIONS:
380
+ package_command = _node_test_command(root, path)
381
+ if package_command:
382
+ commands.append(package_command)
383
+ elif suffix == ".java":
384
+ commands.extend(_java_commands(root, path))
385
+ return commands
386
+
387
+
388
+ def _go_test_command(rel_path: str) -> dict[str, Any]:
389
+ parent = PurePosixPath(rel_path).parent.as_posix()
390
+ package = "./" + parent if parent not in {"", "."} else "."
391
+ return _command(
392
+ f"go test {package}",
393
+ scope="targeted",
394
+ confidence=0.78,
395
+ reason="Go tests run at package directory granularity",
396
+ )
397
+
398
+
399
+ def _node_test_command(root: Path, path: str) -> dict[str, Any] | None:
400
+ script = _package_test_script(root)
401
+ if not script:
402
+ return None
403
+ manager = _node_package_manager(root)
404
+ if manager == "npm":
405
+ command = f"npm test -- {path}"
406
+ elif manager == "yarn":
407
+ command = f"yarn test {path}"
408
+ elif manager == "pnpm":
409
+ command = f"pnpm test -- {path}"
410
+ else:
411
+ command = f"bun test {path}"
412
+ return _command(
413
+ command,
414
+ scope="targeted",
415
+ confidence=0.58,
416
+ reason=f"{manager} test script exists; path filtering is framework-dependent",
417
+ )
418
+
419
+
420
+ def _java_commands(root: Path, path: str) -> list[dict[str, Any]]:
421
+ stem = PurePosixPath(path).stem
422
+ if (root / "gradlew").exists():
423
+ return [
424
+ _command(
425
+ f"./gradlew test --tests '*{stem}'",
426
+ scope="targeted",
427
+ confidence=0.56,
428
+ reason="Gradle wrapper exists and Java test class was matched",
429
+ )
430
+ ]
431
+ if (root / "pom.xml").exists():
432
+ return [
433
+ _command(
434
+ f"mvn -Dtest={stem} test",
435
+ scope="targeted",
436
+ confidence=0.56,
437
+ reason="Maven project and Java test class was matched",
438
+ )
439
+ ]
440
+ return []
441
+
442
+
443
+ def _broad_commands(root: Path) -> list[dict[str, Any]]:
444
+ commands: list[dict[str, Any]] = []
445
+ try:
446
+ scan = scan_workspace(context=resolve_workspace_context(root))
447
+ except Exception:
448
+ scan = None
449
+ if scan is not None:
450
+ for command in scan.likely_test_commands[:6]:
451
+ commands.append(
452
+ _command(
453
+ command,
454
+ scope="broad",
455
+ confidence=0.74,
456
+ reason="repo scan discovered a likely native test command",
457
+ )
458
+ )
459
+ if commands:
460
+ return _dedupe_commands(commands)
461
+ if (
462
+ (root / "pyproject.toml").exists()
463
+ or (root / "pytest.ini").exists()
464
+ or (root / "tests").is_dir()
465
+ ):
466
+ commands.append(
467
+ _command(
468
+ "python -m pytest -q",
469
+ scope="broad",
470
+ confidence=0.7,
471
+ reason="Python test surface detected",
472
+ )
473
+ )
474
+ if (root / "package.json").exists():
475
+ manager = _node_package_manager(root)
476
+ commands.append(
477
+ _command(
478
+ f"{manager} test",
479
+ scope="broad",
480
+ confidence=0.62,
481
+ reason="package.json test surface detected",
482
+ )
483
+ )
484
+ if (root / "go.mod").exists():
485
+ commands.append(
486
+ _command("go test ./...", scope="broad", confidence=0.76, reason="go.mod detected")
487
+ )
488
+ if (root / "Cargo.toml").exists():
489
+ commands.append(
490
+ _command("cargo test", scope="broad", confidence=0.72, reason="Cargo.toml detected")
491
+ )
492
+ if (root / "gradlew").exists():
493
+ commands.append(
494
+ _command(
495
+ "./gradlew test", scope="broad", confidence=0.62, reason="Gradle wrapper detected"
496
+ )
497
+ )
498
+ elif (root / "pom.xml").exists():
499
+ commands.append(
500
+ _command("mvn test", scope="broad", confidence=0.62, reason="pom.xml detected")
501
+ )
502
+ return _dedupe_commands(commands)
503
+
504
+
505
+ def _frameworks_from_commands(commands: list[dict[str, Any]]) -> set[str]:
506
+ frameworks: set[str] = set()
507
+ for item in commands:
508
+ command = str(item.get("command") or "").casefold()
509
+ if "pytest" in command:
510
+ frameworks.add("pytest")
511
+ if "npm" in command or "yarn" in command or "pnpm" in command or "bun" in command:
512
+ frameworks.add("node")
513
+ if "go test" in command:
514
+ frameworks.add("go test")
515
+ if "cargo test" in command:
516
+ frameworks.add("cargo test")
517
+ if "gradle" in command or "mvn" in command:
518
+ frameworks.add("junit")
519
+ return frameworks
520
+
521
+
522
+ def _python_module_name(rel_path: str) -> str:
523
+ pure = PurePosixPath(rel_path)
524
+ if pure.suffix != ".py":
525
+ return ""
526
+ parts = list(pure.with_suffix("").parts)
527
+ if parts and parts[0] in {"src", "lib"}:
528
+ parts = parts[1:]
529
+ if parts and parts[-1] == "__init__":
530
+ parts = parts[:-1]
531
+ return ".".join(part for part in parts if part.isidentifier())
532
+
533
+
534
+ def _node_frameworks(root: Path) -> set[str]:
535
+ script = _package_test_script(root).casefold()
536
+ frameworks = {"node"}
537
+ if "vitest" in script:
538
+ frameworks.add("vitest")
539
+ if "jest" in script:
540
+ frameworks.add("jest")
541
+ return frameworks
542
+
543
+
544
+ def _node_package_manager(root: Path) -> str:
545
+ if (root / "pnpm-lock.yaml").exists():
546
+ return "pnpm"
547
+ if (root / "yarn.lock").exists():
548
+ return "yarn"
549
+ if (root / "bun.lockb").exists() or (root / "bun.lock").exists():
550
+ return "bun"
551
+ return "npm"
552
+
553
+
554
+ def _package_test_script(root: Path) -> str:
555
+ package_json = root / "package.json"
556
+ try:
557
+ payload = json.loads(package_json.read_text(encoding="utf-8"))
558
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
559
+ return ""
560
+ scripts = payload.get("scripts")
561
+ if not isinstance(scripts, dict):
562
+ return ""
563
+ value = scripts.get("test")
564
+ return str(value or "") if isinstance(value, str) else ""
565
+
566
+
567
+ def _file_contains(path: Path, needle: str) -> bool:
568
+ if not needle:
569
+ return False
570
+ try:
571
+ text = path.read_text(encoding="utf-8", errors="ignore")
572
+ except OSError:
573
+ return False
574
+ return needle in text
575
+
576
+
577
+ def _is_ignored(rel_path: str) -> bool:
578
+ parts = set(PurePosixPath(rel_path).parts)
579
+ return bool(parts & {".git", ".venv", "venv", "node_modules", "__pycache__", ".ruff_cache"})
580
+
581
+
582
+ def _command(command: str, *, scope: str, confidence: float, reason: str) -> dict[str, Any]:
583
+ return {
584
+ "command": command,
585
+ "scope": scope,
586
+ "confidence": round(float(confidence), 2),
587
+ "reason": reason,
588
+ }
589
+
590
+
591
+ def _dedupe(values: list[str]) -> list[str]:
592
+ seen: set[str] = set()
593
+ deduped: list[str] = []
594
+ for value in values:
595
+ clean = str(value or "").strip()
596
+ if not clean or clean in seen:
597
+ continue
598
+ seen.add(clean)
599
+ deduped.append(clean)
600
+ return deduped
601
+
602
+
603
+ def _dedupe_test_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
604
+ by_path: dict[str, dict[str, Any]] = {}
605
+ pathless: list[dict[str, Any]] = []
606
+ for candidate in candidates:
607
+ path = str(candidate.get("path") or "").strip()
608
+ if not path:
609
+ pathless.append(candidate)
610
+ continue
611
+ existing = by_path.get(path)
612
+ if existing is None or _test_candidate_rank(candidate) > _test_candidate_rank(existing):
613
+ by_path[path] = candidate
614
+ return sorted(
615
+ [*by_path.values(), *pathless],
616
+ key=_test_candidate_rank,
617
+ reverse=True,
618
+ )
619
+
620
+
621
+ def _test_candidate_rank(candidate: dict[str, Any]) -> tuple[int, float]:
622
+ has_node_id = 1 if str(candidate.get("id") or "").strip() else 0
623
+ return (has_node_id, float(candidate.get("confidence") or 0))
624
+
625
+
626
+ def _dedupe_commands(commands: list[dict[str, Any]]) -> list[dict[str, Any]]:
627
+ by_command: dict[str, dict[str, Any]] = {}
628
+ for item in commands:
629
+ command = str(item.get("command") or "").strip()
630
+ if not command:
631
+ continue
632
+ existing = by_command.get(command)
633
+ if existing is None or float(item.get("confidence") or 0) > float(
634
+ existing.get("confidence") or 0
635
+ ):
636
+ by_command[command] = item
637
+ return sorted(
638
+ by_command.values(),
639
+ key=lambda item: (item.get("scope") != "targeted", -float(item.get("confidence") or 0)),
640
+ )
641
+
642
+
643
+ test_discover.__test__ = False