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,1152 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shlex
7
+ from collections.abc import Callable
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path, PurePosixPath
10
+ from typing import Any
11
+
12
+ from .file_classification import (
13
+ BROAD_SOURCE_EXTENSIONS,
14
+ CODE_SCAN_SKIP_DIR_NAMES,
15
+ SOURCE_EXTENSIONS_BY_LANGUAGE,
16
+ source_extensions_for_languages,
17
+ )
18
+ from .runtime_artifacts import RUNTIME_ARTIFACT_DIR_NAMES
19
+ from .verification_command_analysis import analyze_verification_command
20
+ from .workspace_context import WorkspaceContext
21
+ from .workspace_provisioning import detect_declared_test_runner, ini_has_section
22
+
23
+ REPO_SCAN_SCHEMA_VERSION = 1
24
+ _MAX_TOP_LEVEL_ENTRIES = 18
25
+ _MAX_README_EXCERPTS = 2
26
+ _MAX_EXCERPT_BYTES = 8192
27
+ _MAX_EXCERPT_CHARS = 1200
28
+ _MAX_EXCERPT_LINES = 24
29
+ _MAX_SUMMARY_FILES = 4
30
+ _MAX_SUMMARY_COMMANDS = 3
31
+ _MAX_MANIFEST_SCAN_DEPTH = 4
32
+ _MAX_MANIFEST_SCAN_DIRS = 160
33
+ _MAX_PYTHON_TEST_SIGNAL_DIRS = 64
34
+ _MAX_PYTHON_TEST_SIGNAL_FILES = 256
35
+ _MAX_REPRESENTATIVE_SOURCE_FILES = 12
36
+ _MAX_SOURCE_DISCOVERY_DIRS = 96
37
+ _MAX_SOURCE_DISCOVERY_DEPTH = 4
38
+ _SKIP_TOP_LEVEL_NAMES = {
39
+ ".git",
40
+ ".hg",
41
+ ".svn",
42
+ "build",
43
+ "dist",
44
+ "node_modules",
45
+ "target",
46
+ "venv",
47
+ } | set(RUNTIME_ARTIFACT_DIR_NAMES)
48
+ _SKIP_RECURSIVE_NAMES = (
49
+ _SKIP_TOP_LEVEL_NAMES
50
+ | set(CODE_SCAN_SKIP_DIR_NAMES)
51
+ | {
52
+ ".idea",
53
+ ".mypy_cache",
54
+ ".pytest_cache",
55
+ ".ruff_cache",
56
+ ".tox",
57
+ ".venv",
58
+ "__pycache__",
59
+ "coverage",
60
+ "vendor",
61
+ }
62
+ )
63
+ _MANIFEST_SPECS: tuple[tuple[str, str], ...] = (
64
+ ("pyproject.toml", "python"),
65
+ ("requirements.txt", "python"),
66
+ ("setup.py", "python"),
67
+ ("setup.cfg", "python"),
68
+ ("pytest.ini", "python"),
69
+ ("tox.ini", "python"),
70
+ ("package.json", "node"),
71
+ ("pnpm-lock.yaml", "node"),
72
+ ("yarn.lock", "node"),
73
+ ("package-lock.json", "node"),
74
+ ("bun.lockb", "node"),
75
+ ("tsconfig.json", "typescript"),
76
+ ("pom.xml", "java"),
77
+ ("go.mod", "go"),
78
+ ("Cargo.toml", "rust"),
79
+ ("Makefile", "build"),
80
+ ("justfile", "build"),
81
+ ("Dockerfile", "docker"),
82
+ ("docker-compose.yml", "docker"),
83
+ ("docker-compose.yaml", "docker"),
84
+ ("compose.yml", "docker"),
85
+ ("compose.yaml", "docker"),
86
+ )
87
+ _RECURSIVE_MANIFEST_FILENAMES = {
88
+ "package.json",
89
+ "package-lock.json",
90
+ "pnpm-lock.yaml",
91
+ "yarn.lock",
92
+ "bun.lockb",
93
+ "tsconfig.json",
94
+ "pom.xml",
95
+ }
96
+ _README_NAMES = ("README.md", "README.rst", "README.txt", "README")
97
+ _MAKE_TARGET_RE = re.compile(r"^([A-Za-z0-9_.-]+)\s*::?\s*$")
98
+ _SOURCE_EXTENSIONS_BY_LANGUAGE = SOURCE_EXTENSIONS_BY_LANGUAGE
99
+ _BROAD_SOURCE_EXTENSIONS = BROAD_SOURCE_EXTENSIONS
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class RepoScanResult:
104
+ schema_version: int
105
+ workspace_root: str
106
+ focus_relpath: str
107
+ workspace_kind: str
108
+ git_root: str | None
109
+ has_head_commit: bool
110
+ current_branch: str | None
111
+ top_level_entries: list[dict[str, str]]
112
+ manifests: list[dict[str, str]]
113
+ readme_paths: list[str]
114
+ readme_excerpts: list[dict[str, str]]
115
+ conventions_path: str | None
116
+ conventions_excerpt: str | None
117
+ language_hints: list[str]
118
+ package_hints: list[str]
119
+ likely_test_commands: list[str]
120
+ observed_paths: list[str]
121
+
122
+ def to_dict(self) -> dict[str, Any]:
123
+ return asdict(self)
124
+
125
+ @classmethod
126
+ def from_dict(cls, raw: dict[str, Any]) -> RepoScanResult:
127
+ return cls(
128
+ schema_version=int(raw.get("schema_version") or 0),
129
+ workspace_root=str(raw.get("workspace_root") or ""),
130
+ focus_relpath=str(raw.get("focus_relpath") or "."),
131
+ workspace_kind=str(raw.get("workspace_kind") or ""),
132
+ git_root=(None if raw.get("git_root") in (None, "") else str(raw.get("git_root"))),
133
+ has_head_commit=bool(raw.get("has_head_commit", False)),
134
+ current_branch=(
135
+ None if raw.get("current_branch") in (None, "") else str(raw.get("current_branch"))
136
+ ),
137
+ top_level_entries=_list_of_dicts(raw.get("top_level_entries")),
138
+ manifests=_list_of_dicts(raw.get("manifests")),
139
+ readme_paths=_list_of_strings(raw.get("readme_paths")),
140
+ readme_excerpts=_list_of_dicts(raw.get("readme_excerpts")),
141
+ conventions_path=(
142
+ None
143
+ if raw.get("conventions_path") in (None, "")
144
+ else str(raw.get("conventions_path"))
145
+ ),
146
+ conventions_excerpt=(
147
+ None
148
+ if raw.get("conventions_excerpt") in (None, "")
149
+ else str(raw.get("conventions_excerpt"))
150
+ ),
151
+ language_hints=_list_of_strings(raw.get("language_hints")),
152
+ package_hints=_list_of_strings(raw.get("package_hints")),
153
+ likely_test_commands=_list_of_strings(raw.get("likely_test_commands")),
154
+ observed_paths=_list_of_strings(raw.get("observed_paths")),
155
+ )
156
+
157
+
158
+ def scan_workspace(*, context: WorkspaceContext) -> RepoScanResult:
159
+ root = context.workspace_root
160
+ search_dirs = _search_dirs(context)
161
+ top_level_entries = _collect_top_level_entries(root)
162
+ manifests = _collect_manifests(root=root, search_dirs=search_dirs)
163
+ readme_paths = _collect_readmes(root=root, search_dirs=search_dirs)
164
+ readme_excerpts = [
165
+ {"path": rel_path, "excerpt": excerpt}
166
+ for rel_path, excerpt in (
167
+ _excerpt_entry(root=root, path=root / rel_path)
168
+ for rel_path in readme_paths[:_MAX_README_EXCERPTS]
169
+ )
170
+ if excerpt
171
+ ]
172
+ conventions_path = _find_conventions_path(root=root, search_dirs=search_dirs)
173
+ conventions_excerpt = None
174
+ if conventions_path is not None:
175
+ conventions_excerpt = _read_text_excerpt(root / conventions_path)
176
+ language_hints = _infer_language_hints(manifests=manifests)
177
+ representative_source_paths = _collect_representative_source_paths(
178
+ root=root,
179
+ search_dirs=search_dirs,
180
+ language_hints=language_hints,
181
+ )
182
+ package_hints = _infer_package_hints(root=root, manifests=manifests)
183
+ likely_test_commands = _infer_test_commands(
184
+ root=root,
185
+ search_dirs=search_dirs,
186
+ manifests=manifests,
187
+ )
188
+ observed_paths = _observed_paths(
189
+ top_level_entries=top_level_entries,
190
+ manifests=manifests,
191
+ readme_paths=readme_paths,
192
+ conventions_path=conventions_path,
193
+ representative_source_paths=representative_source_paths,
194
+ )
195
+ return RepoScanResult(
196
+ schema_version=REPO_SCAN_SCHEMA_VERSION,
197
+ workspace_root=os.fspath(root),
198
+ focus_relpath=context.focus_relpath,
199
+ workspace_kind=context.workspace_kind,
200
+ git_root=(os.fspath(context.git_root) if context.git_root is not None else None),
201
+ has_head_commit=context.has_head_commit,
202
+ current_branch=context.current_branch,
203
+ top_level_entries=top_level_entries,
204
+ manifests=manifests,
205
+ readme_paths=readme_paths,
206
+ readme_excerpts=readme_excerpts,
207
+ conventions_path=conventions_path,
208
+ conventions_excerpt=conventions_excerpt,
209
+ language_hints=language_hints,
210
+ package_hints=package_hints,
211
+ likely_test_commands=likely_test_commands,
212
+ observed_paths=observed_paths,
213
+ )
214
+
215
+
216
+ def render_repo_scan_markdown(scan: RepoScanResult) -> str:
217
+ branch_label = scan.current_branch or ("(no HEAD)" if not scan.has_head_commit else "(unknown)")
218
+ lines = [
219
+ "# Workspace Summary",
220
+ "",
221
+ f"- Workspace Root: `{scan.workspace_root}`",
222
+ f"- Focus Directory: `{scan.focus_relpath or '.'}`",
223
+ f"- Workspace Kind: `{scan.workspace_kind}`",
224
+ f"- Git Root: `{scan.git_root or '(none)'}`",
225
+ f"- Current Branch: `{branch_label}`",
226
+ "",
227
+ "## Important Files",
228
+ "",
229
+ ]
230
+ important_files: list[str] = []
231
+ important_files.extend(item.get("path", "") for item in scan.manifests[:8])
232
+ important_files.extend(scan.readme_paths[:4])
233
+ if scan.conventions_path:
234
+ important_files.append(scan.conventions_path)
235
+ important_files.extend(_representative_observed_source_paths(scan.observed_paths)[:8])
236
+ important_files = _unique_nonempty(important_files)
237
+ if important_files:
238
+ for rel_path in important_files:
239
+ lines.append(f"- `{rel_path}`")
240
+ else:
241
+ lines.append("- (none)")
242
+
243
+ lines.extend(["", "## Language Hints", ""])
244
+ if scan.language_hints:
245
+ for hint in scan.language_hints:
246
+ lines.append(f"- `{hint}`")
247
+ else:
248
+ lines.append("- (none)")
249
+
250
+ lines.extend(["", "## Likely Verification Commands", ""])
251
+ if scan.likely_test_commands:
252
+ for command in scan.likely_test_commands:
253
+ lines.append(f"- `{command}`")
254
+ else:
255
+ lines.append("- (none)")
256
+
257
+ if scan.readme_excerpts:
258
+ lines.extend(["", "## README Excerpts", ""])
259
+ for item in scan.readme_excerpts:
260
+ rel_path = str(item.get("path") or "").strip() or "(unknown)"
261
+ excerpt = str(item.get("excerpt") or "").strip()
262
+ lines.append(f"### `{rel_path}`")
263
+ lines.append("")
264
+ lines.append(excerpt or "(empty)")
265
+ lines.append("")
266
+
267
+ if scan.conventions_path:
268
+ lines.extend(["## CONVENTIONS.md Excerpt", ""])
269
+ lines.append(f"`{scan.conventions_path}`")
270
+ lines.append("")
271
+ lines.append(scan.conventions_excerpt or "(empty)")
272
+ lines.append("")
273
+
274
+ return "\n".join(lines).rstrip() + "\n"
275
+
276
+
277
+ def render_repo_scan_summary_lines(scan: RepoScanResult) -> list[str]:
278
+ branch_detail = (
279
+ scan.current_branch
280
+ if scan.current_branch
281
+ else ("no HEAD" if not scan.has_head_commit else "unknown branch")
282
+ )
283
+ lines = [
284
+ f"Workspace root: {scan.workspace_root}",
285
+ f"Focus directory: {scan.focus_relpath or '.'}",
286
+ f"Workspace kind: {scan.workspace_kind} ({branch_detail})",
287
+ ]
288
+ signals = _unique_nonempty(
289
+ [item.get("path", "") for item in scan.manifests[:_MAX_SUMMARY_FILES]]
290
+ + scan.readme_paths[:_MAX_SUMMARY_FILES]
291
+ + ([scan.conventions_path] if scan.conventions_path else [])
292
+ )
293
+ if signals:
294
+ lines.append(f"Important files: {', '.join(signals[:_MAX_SUMMARY_FILES])}")
295
+ if scan.likely_test_commands:
296
+ lines.append(
297
+ "Likely verify: " + ", ".join(scan.likely_test_commands[:_MAX_SUMMARY_COMMANDS])
298
+ )
299
+ return lines
300
+
301
+
302
+ def _list_of_strings(raw: Any) -> list[str]:
303
+ if not isinstance(raw, list):
304
+ return []
305
+ out: list[str] = []
306
+ for item in raw:
307
+ text = str(item or "").strip()
308
+ if text:
309
+ out.append(text)
310
+ return out
311
+
312
+
313
+ def _list_of_dicts(raw: Any) -> list[dict[str, str]]:
314
+ if not isinstance(raw, list):
315
+ return []
316
+ out: list[dict[str, str]] = []
317
+ for item in raw:
318
+ if not isinstance(item, dict):
319
+ continue
320
+ out.append({str(key): str(value) for key, value in item.items() if value is not None})
321
+ return out
322
+
323
+
324
+ def _search_dirs(context: WorkspaceContext) -> list[Path]:
325
+ dirs = [context.workspace_root]
326
+ if context.focus_path != context.workspace_root:
327
+ dirs.append(context.focus_path)
328
+ return dirs
329
+
330
+
331
+ def _collect_top_level_entries(root: Path) -> list[dict[str, str]]:
332
+ entries: list[dict[str, str]] = []
333
+ for candidate in sorted(root.iterdir(), key=lambda path: path.name.casefold()):
334
+ if candidate.name in _SKIP_TOP_LEVEL_NAMES:
335
+ continue
336
+ kind = "dir" if candidate.is_dir() else ("file" if candidate.is_file() else "other")
337
+ entries.append({"path": candidate.name, "kind": kind})
338
+ if len(entries) >= _MAX_TOP_LEVEL_ENTRIES:
339
+ break
340
+ return entries
341
+
342
+
343
+ def _source_extensions_for_languages(language_hints: list[str]) -> set[str]:
344
+ return source_extensions_for_languages(language_hints)
345
+
346
+
347
+ def _collect_representative_source_paths(
348
+ *,
349
+ root: Path,
350
+ search_dirs: list[Path],
351
+ language_hints: list[str],
352
+ ) -> list[str]:
353
+ extensions = _source_extensions_for_languages(language_hints)
354
+ seen: set[str] = set()
355
+ out: list[str] = []
356
+ seen_bases: set[Path] = set()
357
+ for base_dir in search_dirs:
358
+ try:
359
+ resolved_base = base_dir.resolve()
360
+ except OSError:
361
+ continue
362
+ if resolved_base in seen_bases:
363
+ continue
364
+ seen_bases.add(resolved_base)
365
+ visited_dirs = 0
366
+ try:
367
+ walker = os.walk(base_dir, topdown=True)
368
+ except OSError:
369
+ continue
370
+ for current_dir, dirnames, filenames in walker:
371
+ visited_dirs += 1
372
+ if visited_dirs > _MAX_SOURCE_DISCOVERY_DIRS:
373
+ dirnames[:] = []
374
+ break
375
+ current_path = Path(current_dir)
376
+ try:
377
+ rel_to_base = current_path.resolve().relative_to(resolved_base)
378
+ except (OSError, ValueError):
379
+ dirnames[:] = []
380
+ continue
381
+ depth = 0 if rel_to_base == Path(".") else len(rel_to_base.parts)
382
+ dirnames[:] = sorted(
383
+ (
384
+ name
385
+ for name in dirnames
386
+ if name not in _SKIP_RECURSIVE_NAMES and not name.startswith(".")
387
+ ),
388
+ key=str.casefold,
389
+ )
390
+ if depth >= _MAX_SOURCE_DISCOVERY_DEPTH:
391
+ dirnames[:] = []
392
+ for filename in sorted(filenames, key=str.casefold):
393
+ if filename.startswith("."):
394
+ continue
395
+ candidate = current_path / filename
396
+ if candidate.suffix.casefold() not in extensions:
397
+ continue
398
+ try:
399
+ rel_path = _relpath(root, candidate)
400
+ except (OSError, ValueError):
401
+ continue
402
+ if rel_path in seen:
403
+ continue
404
+ seen.add(rel_path)
405
+ out.append(rel_path)
406
+ if len(out) >= _MAX_REPRESENTATIVE_SOURCE_FILES:
407
+ return out
408
+ return out
409
+
410
+
411
+ def _collect_manifests(*, root: Path, search_dirs: list[Path]) -> list[dict[str, str]]:
412
+ manifests: list[dict[str, str]] = []
413
+ seen: set[str] = set()
414
+ for directory in search_dirs:
415
+ for filename, kind in _MANIFEST_SPECS:
416
+ candidate = directory / filename
417
+ if not candidate.exists() or not candidate.is_file():
418
+ continue
419
+ rel_path = _relpath(root, candidate)
420
+ if rel_path in seen:
421
+ continue
422
+ seen.add(rel_path)
423
+ manifests.append({"path": rel_path, "kind": kind})
424
+ for rel_path, kind in _walk_nested_manifests(root=root, base_dir=directory):
425
+ if rel_path in seen:
426
+ continue
427
+ seen.add(rel_path)
428
+ manifests.append({"path": rel_path, "kind": kind})
429
+ manifests.sort(key=lambda item: item["path"].casefold())
430
+ return manifests
431
+
432
+
433
+ def _walk_nested_manifests(*, root: Path, base_dir: Path) -> list[tuple[str, str]]:
434
+ results: list[tuple[str, str]] = []
435
+ visited_dirs = 0
436
+ manifest_kind_map = dict(_MANIFEST_SPECS)
437
+ try:
438
+ walker = os.walk(base_dir, topdown=True)
439
+ except OSError:
440
+ return results
441
+
442
+ for current_dir, dirnames, filenames in walker:
443
+ current_path = Path(current_dir)
444
+ try:
445
+ rel_to_base = current_path.resolve().relative_to(base_dir.resolve())
446
+ except ValueError:
447
+ dirnames[:] = []
448
+ continue
449
+ depth = 0 if rel_to_base == Path(".") else len(rel_to_base.parts)
450
+ dirnames[:] = sorted(
451
+ (
452
+ name
453
+ for name in dirnames
454
+ if name not in _SKIP_RECURSIVE_NAMES and not name.startswith(".git")
455
+ ),
456
+ key=str.casefold,
457
+ )
458
+ if depth >= _MAX_MANIFEST_SCAN_DEPTH:
459
+ dirnames[:] = []
460
+ visited_dirs += 1
461
+ if visited_dirs > _MAX_MANIFEST_SCAN_DIRS:
462
+ break
463
+ for filename in sorted(filenames, key=str.casefold):
464
+ if filename not in _RECURSIVE_MANIFEST_FILENAMES:
465
+ continue
466
+ candidate = current_path / filename
467
+ if not candidate.is_file():
468
+ continue
469
+ kind = manifest_kind_map.get(filename)
470
+ if not kind:
471
+ continue
472
+ results.append((_relpath(root, candidate), kind))
473
+ return results
474
+
475
+
476
+ def _collect_readmes(*, root: Path, search_dirs: list[Path]) -> list[str]:
477
+ readmes: list[str] = []
478
+ seen: set[str] = set()
479
+ for directory in search_dirs:
480
+ for candidate in sorted(directory.iterdir(), key=lambda path: path.name.casefold()):
481
+ if not candidate.is_file():
482
+ continue
483
+ if candidate.name.casefold() not in {name.casefold() for name in _README_NAMES}:
484
+ continue
485
+ rel_path = _relpath(root, candidate)
486
+ if rel_path in seen:
487
+ continue
488
+ seen.add(rel_path)
489
+ readmes.append(rel_path)
490
+ return readmes
491
+
492
+
493
+ def _find_conventions_path(*, root: Path, search_dirs: list[Path]) -> str | None:
494
+ for directory in reversed(search_dirs):
495
+ candidate = directory / "CONVENTIONS.md"
496
+ if candidate.exists() and candidate.is_file():
497
+ return _relpath(root, candidate)
498
+ return None
499
+
500
+
501
+ def _excerpt_entry(*, root: Path, path: Path) -> tuple[str, str]:
502
+ return _relpath(root, path), _read_text_excerpt(path)
503
+
504
+
505
+ def _read_text_excerpt(path: Path) -> str:
506
+ try:
507
+ with path.open("rb") as fh:
508
+ raw = fh.read(_MAX_EXCERPT_BYTES)
509
+ except OSError:
510
+ return ""
511
+ if not raw:
512
+ return ""
513
+ text = raw.decode("utf-8", errors="replace")
514
+ lines = text.splitlines()
515
+ excerpt = "\n".join(lines[:_MAX_EXCERPT_LINES]).strip()
516
+ if len(excerpt) > _MAX_EXCERPT_CHARS:
517
+ excerpt = excerpt[:_MAX_EXCERPT_CHARS].rstrip() + "..."
518
+ return excerpt
519
+
520
+
521
+ def _infer_language_hints(*, manifests: list[dict[str, str]]) -> list[str]:
522
+ kinds = {str(item.get("kind") or "") for item in manifests}
523
+ hints: list[str] = []
524
+ if "python" in kinds:
525
+ hints.append("python")
526
+ if "node" in kinds:
527
+ hints.append("javascript")
528
+ if any(
529
+ item.get("path") == "tsconfig.json" or item.get("path", "").endswith("/tsconfig.json")
530
+ for item in manifests
531
+ ):
532
+ hints.append("typescript")
533
+ if "go" in kinds:
534
+ hints.append("go")
535
+ if "rust" in kinds:
536
+ hints.append("rust")
537
+ if "java" in kinds:
538
+ hints.append("java")
539
+ if "docker" in kinds:
540
+ hints.append("docker")
541
+ return hints
542
+
543
+
544
+ def _infer_package_hints(*, root: Path, manifests: list[dict[str, str]]) -> list[str]:
545
+ manifest_paths = {item.get("path", ""): item.get("kind", "") for item in manifests}
546
+ hints: list[str] = []
547
+ pyproject_path = _find_manifest_path(manifests, "pyproject.toml")
548
+ if pyproject_path is not None:
549
+ tool_names = _pyproject_tool_names(root / pyproject_path)
550
+ if "poetry" in tool_names:
551
+ hints.append("poetry")
552
+ if "uv" in tool_names:
553
+ hints.append("uv")
554
+ if "hatch" in tool_names:
555
+ hints.append("hatch")
556
+ if "setuptools" in tool_names:
557
+ hints.append("setuptools")
558
+ if not tool_names:
559
+ hints.append("python")
560
+ elif any(kind == "python" for kind in manifest_paths.values()):
561
+ hints.append("python")
562
+
563
+ package_json_path = _find_manifest_path(manifests, "package.json")
564
+ if package_json_path is not None:
565
+ hints.append(
566
+ _node_package_manager(
567
+ root=root, package_json_path=package_json_path, manifests=manifests
568
+ )
569
+ )
570
+ elif any(kind == "node" for kind in manifest_paths.values()):
571
+ hints.append("node")
572
+
573
+ if any(kind == "go" for kind in manifest_paths.values()):
574
+ hints.append("go-mod")
575
+ if any(kind == "rust" for kind in manifest_paths.values()):
576
+ hints.append("cargo")
577
+ if any(kind == "java" for kind in manifest_paths.values()):
578
+ hints.append("maven")
579
+ if _find_manifest_path(manifests, "Makefile") is not None:
580
+ hints.append("make")
581
+ if _find_manifest_path(manifests, "justfile") is not None:
582
+ hints.append("just")
583
+ if any(kind == "docker" for kind in manifest_paths.values()):
584
+ hints.append("docker")
585
+ return _unique_nonempty(hints)
586
+
587
+
588
+ def _infer_test_commands(
589
+ *,
590
+ root: Path,
591
+ search_dirs: list[Path],
592
+ manifests: list[dict[str, str]],
593
+ ) -> list[str]:
594
+ commands: list[str] = []
595
+
596
+ makefile_path = _find_manifest_path(manifests, "Makefile")
597
+ if makefile_path is not None:
598
+ target = _preferred_make_target(root / makefile_path)
599
+ if target is not None:
600
+ commands.append(f"make {target}")
601
+
602
+ justfile_path = _find_manifest_path(manifests, "justfile")
603
+ if justfile_path is not None:
604
+ target = _preferred_make_target(root / justfile_path)
605
+ if target is not None:
606
+ commands.append(f"just {target}")
607
+
608
+ for package_json_path in _prefer_leaf_service_manifests(
609
+ _find_manifest_paths(manifests, "package.json")
610
+ ):
611
+ package_path = root / package_json_path
612
+ if _package_json_has_real_test_script(package_path):
613
+ commands.append(
614
+ _node_test_command(
615
+ root=root,
616
+ package_json_path=package_json_path,
617
+ manifests=manifests,
618
+ )
619
+ )
620
+ for script_name in _package_json_safe_verification_scripts(package_path):
621
+ commands.append(
622
+ _node_script_command(
623
+ root=root,
624
+ package_json_path=package_json_path,
625
+ manifests=manifests,
626
+ script_name=script_name,
627
+ )
628
+ )
629
+
630
+ for pom_path in _prefer_leaf_service_manifests(_find_manifest_paths(manifests, "pom.xml")):
631
+ commands.append(_maven_test_command(root=root, pom_path=pom_path))
632
+
633
+ if _find_manifest_path(manifests, "go.mod") is not None:
634
+ commands.append("go test ./...")
635
+
636
+ if _find_manifest_path(manifests, "Cargo.toml") is not None:
637
+ commands.append("cargo test")
638
+
639
+ python_test_command = _python_test_command(
640
+ root=root,
641
+ search_dirs=search_dirs,
642
+ manifests=manifests,
643
+ )
644
+ if python_test_command is not None:
645
+ commands.append(python_test_command)
646
+ commands.extend(_python_configured_check_commands(root=root, manifests=manifests))
647
+
648
+ # Doctest over README/docs files is deliberately never inferred: it asserts
649
+ # that the prose examples still render, not that the code works, so it is
650
+ # never an authoritative verification surface for a change to the code.
651
+
652
+ return _unique_nonempty(commands)
653
+
654
+
655
+ def _preferred_make_target(path: Path) -> str | None:
656
+ excerpt = _read_text_excerpt(path)
657
+ if not excerpt:
658
+ return None
659
+ targets: list[str] = []
660
+ for line in excerpt.splitlines():
661
+ if line.startswith("\t") or line.startswith(" "):
662
+ continue
663
+ match = _MAKE_TARGET_RE.match(line.strip())
664
+ if match:
665
+ targets.append(match.group(1))
666
+ for candidate in ("test", "check", "verify"):
667
+ if candidate in targets:
668
+ return candidate
669
+ return None
670
+
671
+
672
+ def _likely_has_python_tests(
673
+ *,
674
+ root: Path,
675
+ search_dirs: list[Path],
676
+ manifests: list[dict[str, str]],
677
+ ) -> bool:
678
+ return _python_test_command(root=root, search_dirs=search_dirs, manifests=manifests) is not None
679
+
680
+
681
+ def _python_test_command(
682
+ *,
683
+ root: Path,
684
+ search_dirs: list[Path],
685
+ manifests: list[dict[str, str]],
686
+ ) -> str | None:
687
+ declared_runner = detect_declared_test_runner(root)
688
+ for directory in _python_test_search_dirs(root=root, search_dirs=search_dirs):
689
+ if _directory_has_python_test_layout_signal(directory, allow_tests_dir_hint=False):
690
+ rel_path = _relpath(root, directory)
691
+ if declared_runner is None and _directory_declares_unittest_suite(directory):
692
+ if rel_path in {"", "."}:
693
+ return "python -m unittest discover"
694
+ return f"python -m unittest discover -s {shlex.quote(rel_path)}"
695
+ if rel_path in {"", "."}:
696
+ return "pytest -q"
697
+ quoted = shlex.quote(rel_path)
698
+ return f"PYTHONPATH={quoted} pytest -q {quoted}"
699
+ return None
700
+
701
+
702
+ def _python_configured_check_commands(
703
+ *,
704
+ root: Path,
705
+ manifests: list[dict[str, str]],
706
+ ) -> list[str]:
707
+ declared: list[str] = []
708
+ pyproject_path = _find_manifest_path(manifests, "pyproject.toml")
709
+ if pyproject_path is not None:
710
+ tool_names = _pyproject_tool_names(root / pyproject_path)
711
+ if "ruff" in tool_names:
712
+ declared.append("ruff check .")
713
+ if "mypy" in tool_names:
714
+ declared.append("mypy .")
715
+
716
+ setup_cfg_path = _find_manifest_path(manifests, "setup.cfg")
717
+ if setup_cfg_path is not None:
718
+ config_text = _read_bounded_config(root / setup_cfg_path)
719
+ if config_text is not None and ini_has_section(config_text, "ruff"):
720
+ declared.append("ruff check .")
721
+ if config_text is not None and ini_has_section(config_text, "mypy"):
722
+ declared.append("mypy .")
723
+
724
+ safe: list[str] = []
725
+ for command in _unique_nonempty(declared):
726
+ analysis = analyze_verification_command(
727
+ command,
728
+ trusted=True,
729
+ workspace_root=root,
730
+ )
731
+ if analysis.is_valid_verifier:
732
+ safe.append(command)
733
+ return safe
734
+
735
+
736
+ def _read_bounded_config(path: Path) -> str | None:
737
+ try:
738
+ with path.open("rb") as handle:
739
+ raw = handle.read(_MAX_EXCERPT_BYTES)
740
+ except OSError:
741
+ return None
742
+ return raw.decode("utf-8", errors="replace")
743
+
744
+
745
+ def _directory_declares_unittest_suite(directory: Path) -> bool:
746
+ seen_dirs = 0
747
+ seen_files = 0
748
+ saw_unittest = False
749
+ try:
750
+ for current_root, dirnames, filenames in os.walk(directory):
751
+ seen_dirs += 1
752
+ if seen_dirs > _MAX_PYTHON_TEST_SIGNAL_DIRS:
753
+ return False
754
+ dirnames[:] = sorted(
755
+ [name for name in dirnames if name not in _SKIP_RECURSIVE_NAMES],
756
+ key=str.casefold,
757
+ )
758
+ for filename in sorted(filenames, key=str.casefold):
759
+ if not _is_unittest_discovery_signal_file(filename):
760
+ continue
761
+ seen_files += 1
762
+ if seen_files > _MAX_PYTHON_TEST_SIGNAL_FILES:
763
+ return False
764
+ text = _read_bounded_config(Path(current_root) / filename)
765
+ if text is None:
766
+ continue
767
+ if re.search(r"(?m)^(?:async\s+)?def\s+test_", text) or re.search(
768
+ r"(?m)^\s*(?:from\s+pytest\s+import\b|import\s+pytest\b)",
769
+ text,
770
+ ):
771
+ return False
772
+ if re.search(
773
+ r"(?m)^\s*(?:from\s+unittest\s+import\b|import\s+unittest\b)",
774
+ text,
775
+ ):
776
+ saw_unittest = True
777
+ except OSError:
778
+ return False
779
+ return saw_unittest
780
+
781
+
782
+ def detect_fallback_test_commands(*, root: Path) -> list[str]:
783
+ """Last-resort runner detection for a workspace with no usable command.
784
+
785
+ Deliberately *not* part of the primary inference above, which stays
786
+ conservative on purpose: a repo that declares pytest but ships no tests
787
+ should not advertise a verification contract it cannot honour. This chain is
788
+ consulted only once the selected command has turned out to be unusable, when
789
+ the alternative is running with no verification surface at all -- there, a
790
+ runner the repo declares (or one ``unittest`` can discover) is strictly
791
+ better than nothing, and a runner that collects zero tests is reported as
792
+ skipped rather than passed.
793
+ """
794
+ workspace_root = Path(root)
795
+ if detect_declared_test_runner(workspace_root) is not None:
796
+ return ["pytest -q"]
797
+ if _directory_has_python_test_layout_signal(workspace_root, allow_tests_dir_hint=False):
798
+ return ["pytest -q"]
799
+ if _directory_has_unittest_discovery_signal(workspace_root):
800
+ return ["python -m unittest discover"]
801
+ return []
802
+
803
+
804
+ def _python_test_search_dirs(*, root: Path, search_dirs: list[Path]) -> list[Path]:
805
+ focused: list[Path] = []
806
+ workspace_roots: list[Path] = []
807
+ for directory in search_dirs:
808
+ rel_path = _relpath(root, directory)
809
+ if rel_path in {"", "."}:
810
+ workspace_roots.append(directory)
811
+ else:
812
+ focused.append(directory)
813
+ return [*focused, *workspace_roots]
814
+
815
+
816
+ def _directory_has_python_test_layout_signal(
817
+ directory: Path,
818
+ *,
819
+ allow_tests_dir_hint: bool,
820
+ ) -> bool:
821
+ try:
822
+ for candidate in sorted(directory.iterdir(), key=lambda path: path.name.casefold()):
823
+ if candidate.is_file():
824
+ if _is_python_test_signal_file(candidate.name):
825
+ return True
826
+ continue
827
+ if not candidate.is_dir() or candidate.name != "tests":
828
+ continue
829
+ if allow_tests_dir_hint:
830
+ return True
831
+ if _tests_dir_has_python_signal(candidate):
832
+ return True
833
+ except OSError:
834
+ return False
835
+ return False
836
+
837
+
838
+ def _is_python_test_signal_file(name: str) -> bool:
839
+ lowered = str(name or "").strip().casefold()
840
+ if lowered == "conftest.py":
841
+ return True
842
+ return lowered.endswith(".py") and (lowered.startswith("test_") or lowered.endswith("_test.py"))
843
+
844
+
845
+ def _is_unittest_discovery_signal_file(name: str) -> bool:
846
+ """Mirror ``unittest discover``'s own default pattern, ``test*.py``.
847
+
848
+ Deliberately the runner's rule rather than a hand-written guess at what a
849
+ test module looks like: the point of this layer is "would ``unittest``
850
+ collect anything here", and only ``unittest``'s pattern answers that. A
851
+ helper module that happens to match collects zero tests, which the verifier
852
+ already reports as skipped rather than passed.
853
+ """
854
+ lowered = str(name or "").strip().casefold()
855
+ return lowered.startswith("test") and lowered.endswith(".py")
856
+
857
+
858
+ def _directory_has_unittest_discovery_signal(directory: Path) -> bool:
859
+ try:
860
+ for candidate in sorted(directory.iterdir(), key=lambda path: path.name.casefold()):
861
+ if candidate.is_file():
862
+ if _is_unittest_discovery_signal_file(candidate.name):
863
+ return True
864
+ continue
865
+ if not candidate.is_dir() or candidate.name != "tests":
866
+ continue
867
+ if _tests_dir_has_python_signal(candidate, match=_is_unittest_discovery_signal_file):
868
+ return True
869
+ except OSError:
870
+ return False
871
+ return False
872
+
873
+
874
+ def _tests_dir_has_python_signal(
875
+ tests_dir: Path,
876
+ *,
877
+ match: Callable[[str], bool] = _is_python_test_signal_file,
878
+ ) -> bool:
879
+ seen_dirs = 0
880
+ seen_files = 0
881
+ try:
882
+ for current_root, dirnames, filenames in os.walk(tests_dir):
883
+ seen_dirs += 1
884
+ if seen_dirs > _MAX_PYTHON_TEST_SIGNAL_DIRS:
885
+ break
886
+ dirnames[:] = sorted(
887
+ [name for name in dirnames if name not in _SKIP_RECURSIVE_NAMES],
888
+ key=str.casefold,
889
+ )
890
+ for filename in sorted(filenames, key=str.casefold):
891
+ seen_files += 1
892
+ if seen_files > _MAX_PYTHON_TEST_SIGNAL_FILES:
893
+ return False
894
+ if match(filename):
895
+ return True
896
+ _ = current_root
897
+ except OSError:
898
+ return False
899
+ return False
900
+
901
+
902
+ def _package_json_has_real_test_script(path: Path) -> bool:
903
+ payload = _read_json_object(path)
904
+ if payload is None:
905
+ return False
906
+ scripts = payload.get("scripts")
907
+ if not isinstance(scripts, dict):
908
+ return False
909
+ raw = scripts.get("test")
910
+ if not isinstance(raw, str):
911
+ return False
912
+ script = raw.strip()
913
+ if not script:
914
+ return False
915
+ lowered = script.casefold()
916
+ if "no test specified" in lowered:
917
+ return False
918
+ return True
919
+
920
+
921
+ def _package_json_safe_verification_scripts(path: Path) -> list[str]:
922
+ payload = _read_json_object(path)
923
+ scripts = payload.get("scripts") if payload is not None else None
924
+ if not isinstance(scripts, dict):
925
+ return []
926
+ candidate_names = [name for name in ("check", "lint", "typecheck") if name in scripts]
927
+ candidate_names.extend(
928
+ sorted(
929
+ (str(name) for name in scripts if isinstance(name, str) and name.startswith("test:")),
930
+ key=str.casefold,
931
+ )
932
+ )
933
+ safe_names: list[str] = []
934
+ for name in candidate_names:
935
+ script = scripts.get(name)
936
+ if not isinstance(script, str) or not script.strip():
937
+ continue
938
+ analysis = analyze_verification_command(
939
+ script,
940
+ trusted=True,
941
+ workspace_root=path.parent,
942
+ )
943
+ if analysis.is_valid_verifier:
944
+ safe_names.append(name)
945
+ return safe_names
946
+
947
+
948
+ def _node_package_manager(
949
+ *,
950
+ root: Path,
951
+ package_json_path: str,
952
+ manifests: list[dict[str, str]],
953
+ ) -> str:
954
+ payload = _read_json_object(root / package_json_path)
955
+ if payload is not None:
956
+ raw_manager = payload.get("packageManager")
957
+ if isinstance(raw_manager, str):
958
+ lowered = raw_manager.strip().casefold()
959
+ if lowered.startswith("pnpm@"):
960
+ return "pnpm"
961
+ if lowered.startswith("yarn@"):
962
+ return "yarn"
963
+ if lowered.startswith("bun@"):
964
+ return "bun"
965
+ if lowered.startswith("npm@"):
966
+ return "npm"
967
+ manifest_paths = {item.get("path", "") for item in manifests}
968
+ package_dir = PurePosixPath(package_json_path).parent
969
+ search_dirs = [package_dir, *package_dir.parents]
970
+ for candidate_dir in search_dirs:
971
+ prefix = "" if str(candidate_dir) in {"", "."} else f"{candidate_dir.as_posix()}/"
972
+ if f"{prefix}pnpm-lock.yaml" in manifest_paths:
973
+ return "pnpm"
974
+ if f"{prefix}yarn.lock" in manifest_paths:
975
+ return "yarn"
976
+ if f"{prefix}bun.lockb" in manifest_paths:
977
+ return "bun"
978
+ if f"{prefix}package-lock.json" in manifest_paths:
979
+ return "npm"
980
+ return "npm"
981
+
982
+
983
+ def _node_test_command(
984
+ *,
985
+ root: Path,
986
+ package_json_path: str,
987
+ manifests: list[dict[str, str]],
988
+ ) -> str:
989
+ manager = _node_package_manager(
990
+ root=root, package_json_path=package_json_path, manifests=manifests
991
+ )
992
+ package_dir = PurePosixPath(package_json_path).parent.as_posix()
993
+ if package_dir in {"", "."}:
994
+ return f"{manager} test"
995
+ quoted_dir = shlex.quote(package_dir)
996
+ if manager == "pnpm":
997
+ return f"pnpm --dir {quoted_dir} test"
998
+ if manager == "yarn":
999
+ return f"yarn --cwd {quoted_dir} test"
1000
+ if manager == "bun":
1001
+ return f"bun --cwd {quoted_dir} test"
1002
+ return f"npm --prefix {quoted_dir} test"
1003
+
1004
+
1005
+ def _node_script_command(
1006
+ *,
1007
+ root: Path,
1008
+ package_json_path: str,
1009
+ manifests: list[dict[str, str]],
1010
+ script_name: str,
1011
+ ) -> str:
1012
+ manager = _node_package_manager(
1013
+ root=root,
1014
+ package_json_path=package_json_path,
1015
+ manifests=manifests,
1016
+ )
1017
+ quoted_script = shlex.quote(script_name)
1018
+ package_dir = PurePosixPath(package_json_path).parent.as_posix()
1019
+ if package_dir in {"", "."}:
1020
+ return f"{manager} run {quoted_script}"
1021
+ quoted_dir = shlex.quote(package_dir)
1022
+ if manager == "pnpm":
1023
+ return f"pnpm --dir {quoted_dir} run {quoted_script}"
1024
+ if manager == "yarn":
1025
+ return f"yarn --cwd {quoted_dir} run {quoted_script}"
1026
+ if manager == "bun":
1027
+ return f"bun --cwd {quoted_dir} run {quoted_script}"
1028
+ return f"npm --prefix {quoted_dir} run {quoted_script}"
1029
+
1030
+
1031
+ def _maven_test_command(*, root: Path, pom_path: str) -> str:
1032
+ wrapper = _find_maven_wrapper(root=root, pom_path=pom_path)
1033
+ if pom_path == "pom.xml":
1034
+ return f"{wrapper} test" if wrapper else "mvn test"
1035
+ quoted_pom = shlex.quote(pom_path)
1036
+ return f"{wrapper} -f {quoted_pom} test" if wrapper else f"mvn -f {quoted_pom} test"
1037
+
1038
+
1039
+ def _find_maven_wrapper(*, root: Path, pom_path: str) -> str | None:
1040
+ pom_dir = PurePosixPath(pom_path).parent
1041
+ candidate_paths: list[PurePosixPath] = []
1042
+ if str(pom_dir) not in {"", "."}:
1043
+ candidate_paths.append(pom_dir / "mvnw")
1044
+ candidate_paths.append(PurePosixPath("mvnw"))
1045
+ for rel_path in candidate_paths:
1046
+ candidate = root / rel_path.as_posix()
1047
+ if candidate.exists() and candidate.is_file():
1048
+ rendered = rel_path.as_posix()
1049
+ return rendered if rendered.startswith("./") else f"./{rendered}"
1050
+ return None
1051
+
1052
+
1053
+ def _pyproject_tool_names(path: Path) -> set[str]:
1054
+ try:
1055
+ import tomllib
1056
+ except ImportError: # pragma: no cover
1057
+ return set()
1058
+
1059
+ try:
1060
+ with path.open("rb") as fh:
1061
+ raw = fh.read(_MAX_EXCERPT_BYTES)
1062
+ except OSError:
1063
+ return set()
1064
+ try:
1065
+ payload = tomllib.loads(raw.decode("utf-8", errors="replace"))
1066
+ except Exception:
1067
+ return set()
1068
+ tool = payload.get("tool")
1069
+ if not isinstance(tool, dict):
1070
+ return set()
1071
+ return {str(key).strip().casefold() for key in tool if str(key).strip()}
1072
+
1073
+
1074
+ def _read_json_object(path: Path) -> dict[str, Any] | None:
1075
+ try:
1076
+ with path.open("rb") as fh:
1077
+ raw = fh.read(_MAX_EXCERPT_BYTES)
1078
+ except OSError:
1079
+ return None
1080
+ try:
1081
+ parsed = json.loads(raw.decode("utf-8", errors="replace"))
1082
+ except json.JSONDecodeError:
1083
+ return None
1084
+ if not isinstance(parsed, dict):
1085
+ return None
1086
+ return parsed
1087
+
1088
+
1089
+ def _find_manifest_path(manifests: list[dict[str, str]], filename: str) -> str | None:
1090
+ suffix = f"/{filename}"
1091
+ for item in manifests:
1092
+ rel_path = str(item.get("path") or "")
1093
+ if rel_path == filename or rel_path.endswith(suffix):
1094
+ return rel_path
1095
+ return None
1096
+
1097
+
1098
+ def _find_manifest_paths(manifests: list[dict[str, str]], filename: str) -> list[str]:
1099
+ suffix = f"/{filename}"
1100
+ matches: list[str] = []
1101
+ for item in manifests:
1102
+ rel_path = str(item.get("path") or "")
1103
+ if rel_path == filename or rel_path.endswith(suffix):
1104
+ matches.append(rel_path)
1105
+ return matches
1106
+
1107
+
1108
+ def _prefer_leaf_service_manifests(paths: list[str]) -> list[str]:
1109
+ if not paths:
1110
+ return []
1111
+ nested = [path for path in paths if "/" in path]
1112
+ return nested if nested else paths
1113
+
1114
+
1115
+ def _observed_paths(
1116
+ *,
1117
+ top_level_entries: list[dict[str, str]],
1118
+ manifests: list[dict[str, str]],
1119
+ readme_paths: list[str],
1120
+ conventions_path: str | None,
1121
+ representative_source_paths: list[str],
1122
+ ) -> list[str]:
1123
+ observed = _unique_nonempty(
1124
+ [item.get("path", "") for item in top_level_entries]
1125
+ + [item.get("path", "") for item in manifests]
1126
+ + readme_paths
1127
+ + ([conventions_path] if conventions_path else [])
1128
+ + representative_source_paths
1129
+ )
1130
+ return observed
1131
+
1132
+
1133
+ def _representative_observed_source_paths(paths: list[str]) -> list[str]:
1134
+ return [
1135
+ path for path in paths if PurePosixPath(path).suffix.casefold() in _BROAD_SOURCE_EXTENSIONS
1136
+ ]
1137
+
1138
+
1139
+ def _relpath(root: Path, path: Path) -> str:
1140
+ return path.resolve().relative_to(root.resolve()).as_posix()
1141
+
1142
+
1143
+ def _unique_nonempty(values: list[str | None]) -> list[str]:
1144
+ out: list[str] = []
1145
+ seen: set[str] = set()
1146
+ for raw in values:
1147
+ value = str(raw or "").strip()
1148
+ if not value or value in seen:
1149
+ continue
1150
+ seen.add(value)
1151
+ out.append(value)
1152
+ return out