openlanes 3.0.0

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 (466) hide show
  1. checksums.yaml +7 -0
  2. data/agents/hansolo-reviewer.md +59 -0
  3. data/agents/lando-broker.md +70 -0
  4. data/agents/leah-researcher.md +60 -0
  5. data/agents/luke-backend.md +73 -0
  6. data/agents/palpatine-planner.md +61 -0
  7. data/agents/r2d2-mechanic.md +57 -0
  8. data/agents/rey-frontend.md +68 -0
  9. data/agents/yoda-writer.md +67 -0
  10. data/bin/factory-e2e +157 -0
  11. data/bin/plan-number +164 -0
  12. data/bin/setup +26 -0
  13. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
  14. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
  15. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
  16. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
  17. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
  18. data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
  19. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
  20. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
  21. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
  22. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
  23. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
  24. data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
  25. data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
  26. data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
  27. data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
  28. data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
  29. data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
  30. data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
  31. data/evals/cases/lando-broker/unblock-fast.yml +44 -0
  32. data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
  33. data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
  34. data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
  35. data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
  36. data/evals/cases/leah-researcher/research-deep.yml +51 -0
  37. data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
  38. data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
  39. data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
  40. data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
  41. data/evals/cases/leah-researcher/research-fast.yml +55 -0
  42. data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
  43. data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
  44. data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
  45. data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
  46. data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
  47. data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
  48. data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
  49. data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
  50. data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
  51. data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
  52. data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
  53. data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
  54. data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
  55. data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
  56. data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
  57. data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
  58. data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
  59. data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
  60. data/evals/cases/luke-backend/build-medium.yml +68 -0
  61. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
  62. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
  63. data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
  64. data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
  65. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
  66. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
  67. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
  68. data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
  69. data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
  70. data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
  71. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
  72. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
  73. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
  74. data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
  75. data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
  76. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
  77. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
  78. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
  79. data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
  80. data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
  81. data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
  82. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
  83. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
  84. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
  85. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
  86. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
  87. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
  88. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
  89. data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
  90. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
  91. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
  92. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
  93. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
  94. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
  95. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
  96. data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
  97. data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
  98. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
  99. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
  100. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
  101. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
  102. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
  103. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
  104. data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
  105. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
  106. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
  107. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
  108. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
  109. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
  110. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
  111. data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
  112. data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
  113. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
  114. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
  115. data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
  116. data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
  117. data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
  118. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
  119. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
  120. data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
  121. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
  122. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
  123. data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
  124. data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
  125. data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
  126. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
  127. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
  128. data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
  129. data/evals/cases/rey-frontend/ui-medium.yml +55 -0
  130. data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
  131. data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
  132. data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
  133. data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
  134. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
  135. data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
  136. data/evals/cases/yoda-writer/spec-medium.yml +40 -0
  137. data/exe/depot +1 -0
  138. data/exe/lanes +1 -0
  139. data/exe/openlanes +32 -0
  140. data/lib/openlanes/adapters/base.rb +69 -0
  141. data/lib/openlanes/adapters/claude.rb +124 -0
  142. data/lib/openlanes/adapters/codex.rb +33 -0
  143. data/lib/openlanes/adapters/model.rb +37 -0
  144. data/lib/openlanes/adapters/pi.rb +22 -0
  145. data/lib/openlanes/adapters/stream.rb +196 -0
  146. data/lib/openlanes/adapters.rb +92 -0
  147. data/lib/openlanes/agents/agent.rb +103 -0
  148. data/lib/openlanes/agents/profile.rb +57 -0
  149. data/lib/openlanes/agents/registry.rb +151 -0
  150. data/lib/openlanes/agents/roster.rb +103 -0
  151. data/lib/openlanes/agents/routing.rb +88 -0
  152. data/lib/openlanes/agents.rb +20 -0
  153. data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
  154. data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
  155. data/lib/openlanes/cli/base.rb +263 -0
  156. data/lib/openlanes/cli/create/create.rb +354 -0
  157. data/lib/openlanes/cli/docs/docs.rb +40 -0
  158. data/lib/openlanes/cli/eval/eval.rb +110 -0
  159. data/lib/openlanes/cli/index/index.rb +40 -0
  160. data/lib/openlanes/cli/init/init.rb +145 -0
  161. data/lib/openlanes/cli/linear/linear.rb +36 -0
  162. data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
  163. data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
  164. data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
  165. data/lib/openlanes/cli/mail/mail.rb +31 -0
  166. data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
  167. data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
  168. data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
  169. data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
  170. data/lib/openlanes/cli/run/run.rb +544 -0
  171. data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
  172. data/lib/openlanes/cli/start/start.rb +110 -0
  173. data/lib/openlanes/cli/state/state.rb +26 -0
  174. data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
  175. data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
  176. data/lib/openlanes/cli/states/states.rb +15 -0
  177. data/lib/openlanes/cli/unblock/unblock.rb +235 -0
  178. data/lib/openlanes/cli/version/version.rb +13 -0
  179. data/lib/openlanes/cli/web/web.rb +100 -0
  180. data/lib/openlanes/cli/worktree/worktree.rb +85 -0
  181. data/lib/openlanes/cli.rb +162 -0
  182. data/lib/openlanes/engine/board.rb +104 -0
  183. data/lib/openlanes/engine/dispatcher.rb +1297 -0
  184. data/lib/openlanes/engine/progress_log.rb +125 -0
  185. data/lib/openlanes/engine/runner.rb +224 -0
  186. data/lib/openlanes/engine/state_file.rb +190 -0
  187. data/lib/openlanes/engine/tally.rb +177 -0
  188. data/lib/openlanes/engine.rb +19 -0
  189. data/lib/openlanes/evals/case.rb +191 -0
  190. data/lib/openlanes/evals/changes.rb +67 -0
  191. data/lib/openlanes/evals/check.rb +224 -0
  192. data/lib/openlanes/evals/evidence.rb +71 -0
  193. data/lib/openlanes/evals/live.rb +140 -0
  194. data/lib/openlanes/evals/recording.rb +38 -0
  195. data/lib/openlanes/evals/run.rb +34 -0
  196. data/lib/openlanes/evals/score.rb +24 -0
  197. data/lib/openlanes/evals/scorer.rb +42 -0
  198. data/lib/openlanes/evals/verdict.rb +18 -0
  199. data/lib/openlanes/evals.rb +56 -0
  200. data/lib/openlanes/execution/checks.rb +73 -0
  201. data/lib/openlanes/execution/child.rb +228 -0
  202. data/lib/openlanes/execution/clock.rb +121 -0
  203. data/lib/openlanes/execution/control.rb +194 -0
  204. data/lib/openlanes/execution/executor.rb +849 -0
  205. data/lib/openlanes/execution/prompt.rb +455 -0
  206. data/lib/openlanes/execution/transcript.rb +531 -0
  207. data/lib/openlanes/execution.rb +19 -0
  208. data/lib/openlanes/launcher.rb +69 -0
  209. data/lib/openlanes/lifecycle/adoption.rb +197 -0
  210. data/lib/openlanes/lifecycle/brief.rb +288 -0
  211. data/lib/openlanes/lifecycle/completion.rb +58 -0
  212. data/lib/openlanes/lifecycle/creator.rb +126 -0
  213. data/lib/openlanes/lifecycle/lanes.rb +74 -0
  214. data/lib/openlanes/lifecycle/resync.rb +428 -0
  215. data/lib/openlanes/lifecycle/unblocker.rb +236 -0
  216. data/lib/openlanes/lifecycle.rb +28 -0
  217. data/lib/openlanes/linear/api.rb +224 -0
  218. data/lib/openlanes/linear/attribution.rb +185 -0
  219. data/lib/openlanes/linear/fuzzy.rb +68 -0
  220. data/lib/openlanes/linear/import.rb +314 -0
  221. data/lib/openlanes/linear/issue.rb +187 -0
  222. data/lib/openlanes/linear/mapping.rb +116 -0
  223. data/lib/openlanes/linear/push.rb +197 -0
  224. data/lib/openlanes/linear/survey.rb +174 -0
  225. data/lib/openlanes/linear/unit.rb +274 -0
  226. data/lib/openlanes/linear.rb +41 -0
  227. data/lib/openlanes/live/claims.rb +71 -0
  228. data/lib/openlanes/live/command.rb +122 -0
  229. data/lib/openlanes/live/commands.rb +505 -0
  230. data/lib/openlanes/live/keys.rb +93 -0
  231. data/lib/openlanes/live/memory_store.rb +265 -0
  232. data/lib/openlanes/live/meter.rb +51 -0
  233. data/lib/openlanes/live/node.rb +88 -0
  234. data/lib/openlanes/live/publisher.rb +179 -0
  235. data/lib/openlanes/live/repo.rb +60 -0
  236. data/lib/openlanes/live/run_record.rb +133 -0
  237. data/lib/openlanes/live/run_state.rb +101 -0
  238. data/lib/openlanes/live/session.rb +107 -0
  239. data/lib/openlanes/live/snapshot.rb +322 -0
  240. data/lib/openlanes/live/stations.rb +67 -0
  241. data/lib/openlanes/live/store.rb +277 -0
  242. data/lib/openlanes/live.rb +109 -0
  243. data/lib/openlanes/plans/dev_work.rb +56 -0
  244. data/lib/openlanes/plans/feature.rb +131 -0
  245. data/lib/openlanes/plans/index.rb +208 -0
  246. data/lib/openlanes/plans/ledger.rb +255 -0
  247. data/lib/openlanes/plans/mailbox.rb +164 -0
  248. data/lib/openlanes/plans/ordinal.rb +92 -0
  249. data/lib/openlanes/plans/plan_state.rb +291 -0
  250. data/lib/openlanes/plans/pull_request.rb +29 -0
  251. data/lib/openlanes/plans/pull_requests.rb +229 -0
  252. data/lib/openlanes/plans/spec.rb +213 -0
  253. data/lib/openlanes/plans/state_machine.rb +433 -0
  254. data/lib/openlanes/plans/status.rb +365 -0
  255. data/lib/openlanes/plans/subject.rb +133 -0
  256. data/lib/openlanes/plans/tree.rb +79 -0
  257. data/lib/openlanes/plans.rb +39 -0
  258. data/lib/openlanes/presentation/diagram.rb +149 -0
  259. data/lib/openlanes/presentation/documentation.rb +438 -0
  260. data/lib/openlanes/presentation/reporter.rb +181 -0
  261. data/lib/openlanes/presentation/viewer.rb +62 -0
  262. data/lib/openlanes/presentation.rb +23 -0
  263. data/lib/openlanes/skills/installer.rb +68 -0
  264. data/lib/openlanes/skills/skill.rb +60 -0
  265. data/lib/openlanes/skills.rb +25 -0
  266. data/lib/openlanes/support/config.rb +217 -0
  267. data/lib/openlanes/support/frontmatter.rb +38 -0
  268. data/lib/openlanes/support/markdown.rb +56 -0
  269. data/lib/openlanes/support/project.rb +79 -0
  270. data/lib/openlanes/support/ui.rb +671 -0
  271. data/lib/openlanes/vcs/github.rb +171 -0
  272. data/lib/openlanes/vcs/publisher.rb +221 -0
  273. data/lib/openlanes/vcs/worktree.rb +248 -0
  274. data/lib/openlanes/vcs.rb +19 -0
  275. data/lib/openlanes/version.rb +10 -0
  276. data/lib/openlanes/web/app.rb +176 -0
  277. data/lib/openlanes/web/context.rb +66 -0
  278. data/lib/openlanes/web/demo.rb +169 -0
  279. data/lib/openlanes/web/demo_board.rb +286 -0
  280. data/lib/openlanes/web/events.rb +104 -0
  281. data/lib/openlanes/web/launcher.rb +143 -0
  282. data/lib/openlanes/web/server.rb +77 -0
  283. data/lib/openlanes/web/stores.rb +36 -0
  284. data/lib/openlanes/web.rb +92 -0
  285. data/lib/openlanes.rb +151 -0
  286. data/openlanes.gemspec +87 -0
  287. data/schemas/plan-state.schema.json +101 -0
  288. data/skills/openlanes/SKILL.md +48 -0
  289. data/skills/openlanes-documents/SKILL.md +61 -0
  290. data/skills/openlanes-documents/references/blocked-template.md +49 -0
  291. data/skills/openlanes-documents/references/closing-templates.md +59 -0
  292. data/skills/openlanes-documents/references/plan-template.md +61 -0
  293. data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
  294. data/skills/openlanes-documents/references/spec-template.md +45 -0
  295. data/skills/openlanes-run/SKILL.md +90 -0
  296. data/skills/openlanes-setup/SKILL.md +64 -0
  297. data/skills/openlanes-spec/SKILL.md +90 -0
  298. data/skills/openlanes-status/SKILL.md +32 -0
  299. data/skills/openlanes-sync/SKILL.md +22 -0
  300. data/web/dist/assets/apl-B4CMkyY2.js +2 -0
  301. data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  302. data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
  303. data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
  304. data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
  305. data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
  306. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
  307. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
  308. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
  309. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
  310. data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  311. data/web/dist/assets/clike-B9uivgTg.js +2 -0
  312. data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
  313. data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
  314. data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
  315. data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  316. data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  317. data/web/dist/assets/crystal-SjHAIU92.js +2 -0
  318. data/web/dist/assets/css-BnMrqG3P.js +2 -0
  319. data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
  320. data/web/dist/assets/d-pRatUO7H.js +2 -0
  321. data/web/dist/assets/diff-DbItnlRl.js +2 -0
  322. data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  323. data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
  324. data/web/dist/assets/dylan-DwRh75JA.js +2 -0
  325. data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
  326. data/web/dist/assets/ecl-Cabwm37j.js +2 -0
  327. data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
  328. data/web/dist/assets/elm-vLlmbW-K.js +2 -0
  329. data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
  330. data/web/dist/assets/factor-kuTfRLto.js +2 -0
  331. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  332. data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
  333. data/web/dist/assets/forth-Ffai-XNe.js +2 -0
  334. data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
  335. data/web/dist/assets/gas-Bneqetm1.js +2 -0
  336. data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
  337. data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
  338. data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
  339. data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
  340. data/web/dist/assets/http-DBlCnlav.js +2 -0
  341. data/web/dist/assets/idl-BEugSyMb.js +2 -0
  342. data/web/dist/assets/index-2xPC-eHt.js +2 -0
  343. data/web/dist/assets/index-8dNhRmiO.js +8 -0
  344. data/web/dist/assets/index-B7-O9ujQ.js +2 -0
  345. data/web/dist/assets/index-BdR34cXC.js +2 -0
  346. data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
  347. data/web/dist/assets/index-Byyxbh_z.js +4 -0
  348. data/web/dist/assets/index-C0VmkrO-.js +2 -0
  349. data/web/dist/assets/index-CI74IkWP.js +198 -0
  350. data/web/dist/assets/index-CYxcbpRM.js +3 -0
  351. data/web/dist/assets/index-D0rmJN3h.js +2 -0
  352. data/web/dist/assets/index-DB4QwuYn.js +2 -0
  353. data/web/dist/assets/index-DDI7zoAS.js +2 -0
  354. data/web/dist/assets/index-DGLUuEXi.js +2 -0
  355. data/web/dist/assets/index-OR-odsl9.js +2 -0
  356. data/web/dist/assets/index-P3vSkiU-.js +2 -0
  357. data/web/dist/assets/index-a4EHR-u4.css +1 -0
  358. data/web/dist/assets/index-mRV4XhJZ.js +2 -0
  359. data/web/dist/assets/index-popk9wbW.js +2 -0
  360. data/web/dist/assets/index-uM88Njdj.js +2 -0
  361. data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
  362. data/web/dist/assets/julia-DuME0IfC.js +2 -0
  363. data/web/dist/assets/livescript-BwQOo05w.js +2 -0
  364. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  365. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  366. data/web/dist/assets/lua-VAEuO923.js +2 -0
  367. data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
  368. data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
  369. data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
  370. data/web/dist/assets/mllike-CXdrOF99.js +2 -0
  371. data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
  372. data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  373. data/web/dist/assets/mumps-BT43cFF4.js +2 -0
  374. data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
  375. data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
  376. data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
  377. data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
  378. data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
  379. data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
  380. data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
  381. data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
  382. data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
  383. data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
  384. data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
  385. data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
  386. data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
  387. data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
  388. data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
  389. data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
  390. data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
  391. data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
  392. data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
  393. data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
  394. data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
  395. data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
  396. data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
  397. data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
  398. data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
  399. data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
  400. data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
  401. data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
  402. data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
  403. data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
  404. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
  405. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
  406. data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
  407. data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
  408. data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
  409. data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
  410. data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
  411. data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
  412. data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
  413. data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
  414. data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
  415. data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
  416. data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
  417. data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
  418. data/web/dist/assets/oz-BzwKVEFT.js +2 -0
  419. data/web/dist/assets/pascal--L3eBynH.js +2 -0
  420. data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
  421. data/web/dist/assets/pig-CevX1Tat.js +2 -0
  422. data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
  423. data/web/dist/assets/properties-C78fOPTZ.js +2 -0
  424. data/web/dist/assets/protobuf-ChK-085T.js +2 -0
  425. data/web/dist/assets/pug-DeIclll2.js +2 -0
  426. data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
  427. data/web/dist/assets/python-BuPzkPfP.js +2 -0
  428. data/web/dist/assets/q-pXgVlZs6.js +2 -0
  429. data/web/dist/assets/r-B6wPVr8A.js +2 -0
  430. data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
  431. data/web/dist/assets/ruby-BK5aLJev.js +2 -0
  432. data/web/dist/assets/sas-B4kiWyti.js +2 -0
  433. data/web/dist/assets/scheme-C41bIUwD.js +2 -0
  434. data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
  435. data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
  436. data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  437. data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  438. data/web/dist/assets/solr-DehyRSwq.js +2 -0
  439. data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
  440. data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  441. data/web/dist/assets/sql-D0XecflT.js +2 -0
  442. data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
  443. data/web/dist/assets/stylus-B533Al4x.js +2 -0
  444. data/web/dist/assets/swift-BzpIVaGY.js +2 -0
  445. data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
  446. data/web/dist/assets/textile-CnDTJFAw.js +2 -0
  447. data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  448. data/web/dist/assets/tiki-DGYXhP31.js +2 -0
  449. data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
  450. data/web/dist/assets/troff-wAsdV37c.js +2 -0
  451. data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
  452. data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  453. data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
  454. data/web/dist/assets/vb-CmGdzxic.js +2 -0
  455. data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
  456. data/web/dist/assets/velocity-D8B20fx6.js +2 -0
  457. data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
  458. data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
  459. data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
  460. data/web/dist/assets/xquery-DzFWVndE.js +2 -0
  461. data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
  462. data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
  463. data/web/dist/index.html +14 -0
  464. data/web/dist/vite.svg +1 -0
  465. data/web/src/api/fixtures/board.json +512 -0
  466. metadata +907 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Lifecycle
5
+ # Moves a plan past the phases its lane skips.
6
+ #
7
+ # A plan in the `plan` lane has a specification its author already
8
+ # trusts, so research and rewriting are skipped: the harness writes the
9
+ # blank `plan.md` yoda-writer would have left and moves the folder to 📋.
10
+ # A plan in the `quick` lane is a short mechanical task: the harness
11
+ # writes a one-unit `plan.md` and moves the folder to ⭐️, where
12
+ # r2d2-mechanic is the only builder that serves the lane.
13
+ #
14
+ # Both moves are ordinary transitions through the state machine, so the
15
+ # invariants still decide; the harness only supplies the file the
16
+ # skipped agent would have written.
17
+ module Lanes
18
+ # The state each lane jumps to.
19
+ TARGET = { plan: :ready_for_planning, quick: :planned }.freeze
20
+
21
+ # The states a lane jumps from. Anything later has already been worked.
22
+ FROM = %i[new researched].freeze
23
+
24
+ # What the harness writes into `plan.md` for each lane.
25
+ STUB = {
26
+ plan: "",
27
+ quick: <<~MARKDOWN
28
+ # Plan
29
+
30
+ ## Task
31
+
32
+ Quick lane: the task is the whole of `spec.md`, done in one pass by `r2d2-mechanic`.
33
+ MARKDOWN
34
+ }.freeze
35
+
36
+ module_function
37
+
38
+ # Where this plan's lane says it should be, when that is somewhere other
39
+ # than where it is.
40
+ #
41
+ # @param subject [OpenLanes::Plans::Subject]
42
+ # @param spec [OpenLanes::Plans::Spec]
43
+ # @return [OpenLanes::Plans::Status, nil]
44
+ def target(subject, spec = Plans::Spec.for(subject))
45
+ key = TARGET[spec.lane] or return nil
46
+ return nil unless FROM.include?(subject.status.key)
47
+
48
+ Plans.status(key)
49
+ end
50
+
51
+ # Write the skipped agent's file and move the folder.
52
+ #
53
+ # @param subject [OpenLanes::Plans::Subject]
54
+ # @param commit [Boolean] false only reports what would happen
55
+ # @return [Array(Symbol, Symbol), nil] from and to, when it moved
56
+ def advance(subject, commit:)
57
+ spec = Plans::Spec.for(subject)
58
+ to = target(subject, spec) or return nil
59
+ from = subject.status.key
60
+ return [from, to.key] unless commit
61
+
62
+ path = File.join(subject.feature.path, "plan.md")
63
+ unless File.exist?(path)
64
+ File.write(path, [STUB.fetch(spec.lane).rstrip, Completion.markdown(spec)].reject(&:empty?).join("\n\n"))
65
+ end
66
+ machine = subject.machine
67
+ return nil unless machine.may?(to.key)
68
+
69
+ machine.promote!(to.key)
70
+ [from, to.key]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Lifecycle
5
+ # Reconciling what is recorded with what is true.
6
+ #
7
+ # Both resyncs are dry-run by default and both refuse to act where they are
8
+ # not certain, because both write to things other people join on: folder
9
+ # names, and pull request titles.
10
+ module Resync
11
+ # `resync dirs` — makes every folder's name say what the folder is.
12
+ #
13
+ # Two things can be wrong with a name, and both are repaired here:
14
+ #
15
+ # - **The emoji is wrong** — the contents justify a different state. A
16
+ # folder whose status already holds is left alone, which is what stops
17
+ # ⭕️ Blocked and 🅱️ Product Blocked — deliberately identical invariants,
18
+ # distinguished only by the name — from collapsing into one.
19
+ # - **The spelling is not canonical** — a folder written `018-⚪️--foo`
20
+ # before the `NNN.MM` padding rule or the ` → ` separator, or
21
+ # `18.1-⚪️--foo` by hand. Unpadded numbers read fine and sort wrong —
22
+ # `018.09` < `018.1` < `018.10` puts a single mixed-width folder in the
23
+ # middle of the range. Both are renamed, to `018.00-⚪️ → foo` and
24
+ # `018.01-⚪️ → foo`.
25
+ #
26
+ # The repairs are applied in that order reversed, as **up to two renames
27
+ # per folder**: first the spelling, under the status the folder already
28
+ # claims — which is how a `--`-era folder migrates to the ` → ` form —
29
+ # and only then the status its contents justify. Both renames aim at
30
+ # {Feature.plan_dirname}, so they cannot disagree about the final name;
31
+ # splitting them keeps each auditable on its own, instead of one report
32
+ # line where a spelling migration also appears to have opinions about
33
+ # the state of the work.
34
+ class Dirs
35
+ # A proposed rename.
36
+ #
37
+ # @!attribute [r] dirname
38
+ # @return [String] the folder as it stands
39
+ # @!attribute [r] from
40
+ # @return [Symbol] the state it claims
41
+ # @!attribute [r] to
42
+ # @return [Symbol] the state its contents justify
43
+ # @!attribute [r] source
44
+ # @return [String] absolute path now
45
+ # @!attribute [r] target
46
+ # @return [String] absolute path after
47
+ # @!attribute [r] reason
48
+ # @return [String] why the current name is wrong
49
+ Change = Data.define(:dirname, :from, :to, :source, :target, :reason) do
50
+ # @return [String] a single auditable line — `⇒` rather than `→`,
51
+ # which the folder names themselves now contain
52
+ def to_s = "#{dirname} ⇒ #{File.basename(target)} (#{reason})"
53
+ end
54
+
55
+ # The state a folder's contents justify, or the one it claims when
56
+ # nothing fits. What a rename moves toward, and what the dispatcher
57
+ # reads on a dry run, which renames nothing, so the preview still
58
+ # names the agents a real run would start.
59
+ #
60
+ # @param subject [OpenLanes::Plans::Subject]
61
+ # @return [OpenLanes::Plans::Status]
62
+ def self.target(subject) = subject.best_fit || subject.status
63
+
64
+ # @param tree [OpenLanes::Plans::Tree]
65
+ # @param except [Array<String>] ordinals to leave alone however wrong
66
+ # their names are: the dispatcher passes the plans an agent is
67
+ # working in right now
68
+ def initialize(tree:, except: [])
69
+ @tree = tree
70
+ @except = except.map(&:to_s)
71
+ end
72
+
73
+ # @return [OpenLanes::Plans::Tree]
74
+ attr_reader :tree
75
+
76
+ # What would change, without changing anything. A folder can appear
77
+ # twice: its spelling migration first, then its status correction.
78
+ #
79
+ # @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>]
80
+ def plan
81
+ tree.subjects.flat_map do |subject|
82
+ @except.include?(subject.feature.ordinal.to_s) ? [] : changes_for(subject)
83
+ end
84
+ end
85
+
86
+ # @param commit [Boolean] actually rename
87
+ # @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>] what was proposed
88
+ def call(commit: false)
89
+ changes = plan
90
+ return changes unless commit
91
+
92
+ UI.stepping(changes, "Renaming") { |change| rename(change) }
93
+ tree.reload
94
+ changes
95
+ end
96
+
97
+ private
98
+
99
+ # A folder moves when the name it has is not the name it should have.
100
+ # {target} says what that name is; this says how to get there, as up
101
+ # to two renames applied in order. The status rename starts from where
102
+ # the spelling rename left the folder, not from where it stands now,
103
+ # so committing the steps sequentially never renames a path that has
104
+ # already moved out from under it.
105
+ #
106
+ # @param subject [OpenLanes::Plans::Subject]
107
+ # @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>] empty when the name
108
+ # is already right
109
+ def changes_for(subject)
110
+ feature = subject.feature
111
+ fit = self.class.target(subject)
112
+ steps = []
113
+
114
+ spelled = feature.dirname_as(subject.status)
115
+ if spelled != feature.dirname
116
+ steps << step(feature,
117
+ from: subject.status,
118
+ to: subject.status,
119
+ source: feature.path,
120
+ reason: spelling_reason(feature))
121
+ end
122
+
123
+ if fit.key != subject.status.key
124
+ source = steps.empty? ? feature.path : steps.last.target
125
+ steps << step(feature,
126
+ from: subject.status,
127
+ to: fit,
128
+ source:,
129
+ reason: subject.violation || "contents now justify #{fit.label}")
130
+ end
131
+
132
+ steps
133
+ end
134
+
135
+ # @param feature [OpenLanes::Plans::Feature]
136
+ # @param from [OpenLanes::Plans::Status]
137
+ # @param to [OpenLanes::Plans::Status]
138
+ # @param source [String] absolute path the rename starts from
139
+ # @param reason [String]
140
+ # @return [OpenLanes::Lifecycle::Resync::Dirs::Change]
141
+ def step(feature, from:, to:, source:, reason:)
142
+ Change.new(
143
+ dirname: File.basename(source),
144
+ from: from.key,
145
+ to: to.key,
146
+ source:,
147
+ target: File.join(File.dirname(feature.path), feature.dirname_as(to)),
148
+ reason:
149
+ )
150
+ end
151
+
152
+ # @param feature [OpenLanes::Plans::Feature]
153
+ # @return [String] why the spelling is wrong, status aside
154
+ def spelling_reason(feature)
155
+ return "#{feature.dirname_ordinal} is not padded to #{feature.ordinal}" unless feature.padded?
156
+
157
+ "name is not in canonical NNN.MM-<emoji> → <slug> form"
158
+ end
159
+
160
+ # A rename that finds its target occupied has not happened, and saying
161
+ # nothing about it would leave the folder misnamed with a report
162
+ # claiming otherwise. Two folders can want the same canonical name —
163
+ # `018-⚪️--foo` and `018.00-⚪️ → foo` are the same plan written twice.
164
+ #
165
+ # @param change [OpenLanes::Lifecycle::Resync::Dirs::Change]
166
+ # @return [void]
167
+ # @raise [OpenLanes::Error] when the target name is already taken
168
+ def rename(change)
169
+ return if OpenLanes.move_directory(change.source, change.target)
170
+
171
+ raise Error, "cannot rename #{change.dirname} — #{File.basename(change.target)} already exists"
172
+ end
173
+ end
174
+
175
+ # `resync prs` — puts an `[NNN.MM]` prefix on every pull request title that
176
+ # lacks one, and `[DEV.00]` on the ones that implement no plan.
177
+ #
178
+ # The resolution order is the branch name first, then the diff, and only
179
+ # when the diff touches exactly one plan. Anything else is reported for a
180
+ # human and never edited: `pull-requests.md` is generated from these
181
+ # titles, and a wrong number files work under a plan that did not do it,
182
+ # leaving the plan that did looking untouched.
183
+ class Prs
184
+ # The marker this tool used to write, now replaced. A title wearing it is
185
+ # rewritten in place: the assertion it makes — "this belongs to no
186
+ # specification" — is still true and still somebody's deliberate call,
187
+ # so it is restamped rather than re-resolved.
188
+ STALE = /\A\[#{Regexp.escape(OpenLanes::STALE_NO_PLAN_PREFIX)}\]\s*/
189
+
190
+ # Titles that already carry a prefix — a plan number, the no-plan marker,
191
+ # or the legacy `[XXX]`.
192
+ PREFIXED = /\A\[(?:\d{3}(?:\.\d{2})?|#{OpenLanes::NO_PLAN_PREFIX}|XXX)\](?:\([A-Z]\))?\s/
193
+
194
+ # Finds a plan number in a branch name: `kig/018.01-verify`, `002-slug`.
195
+ BRANCH_PATTERN = %r{(?:\A|[/\-_])(\d{3}(?:\.\d{2})?)(?:\z|[-_])}
196
+
197
+ # A proposed retitle.
198
+ #
199
+ # @!attribute [r] number
200
+ # @return [Integer] the pull request
201
+ # @!attribute [r] title
202
+ # @return [String] as it stands
203
+ # @!attribute [r] new_title
204
+ # @return [String, nil] nil when nothing may safely be done
205
+ # @!attribute [r] ordinal
206
+ # @return [OpenLanes::Plans::Ordinal, nil]
207
+ # @!attribute [r] reason
208
+ # @return [String] how it was resolved, or why it was not
209
+ # @!attribute [r] ambiguous
210
+ # @return [Boolean] a human must decide; never edited
211
+ # @!attribute [r] assumed
212
+ # @return [Boolean] "no plan" was inferred, not asserted by the author
213
+ Change = Data.define(:number, :title, :new_title, :ordinal, :reason, :ambiguous, :assumed, :adopted) do
214
+ # @return [Boolean]
215
+ def ambiguous? = ambiguous
216
+
217
+ # @return [Boolean]
218
+ def assumed? = assumed
219
+
220
+ # @return [Boolean] whether a plan folder was minted for this one
221
+ def adopted? = adopted
222
+
223
+ # @return [Boolean] safe to apply without a human looking
224
+ def applicable? = !ambiguous && !new_title.nil?
225
+ end
226
+
227
+ # @param tree [OpenLanes::Plans::Tree]
228
+ # @param github [OpenLanes::Vcs::GitHub]
229
+ # @param adopt [Boolean] give a plan folder to every pull request that
230
+ # resolves to none, rather than flagging it and moving on
231
+ # @param root [String, nil] repository root, for reading branches
232
+ def initialize(tree:, github: Vcs::GitHub.new, adopt: true, root: nil)
233
+ @tree = tree
234
+ @github = github
235
+ @adopt = adopt
236
+ @root = root
237
+ end
238
+
239
+ # @return [Boolean]
240
+ def adopt? = @adopt
241
+
242
+ # @return [OpenLanes::Plans::Tree]
243
+ attr_reader :tree
244
+
245
+ # @return [OpenLanes::Vcs::GitHub]
246
+ attr_reader :github
247
+
248
+ # What would change, without changing anything.
249
+ #
250
+ # @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
251
+ def plan
252
+ restamps + resolve(candidates).then { |changes| adopt? ? with_adoptions(changes) : changes }
253
+ end
254
+
255
+ # @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
256
+ def restamps = stale.map { |pull| restamped(pull) }
257
+
258
+ # @param commit [Boolean] actually retitle, and mint the folders
259
+ # @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>] what was proposed
260
+ def call(commit: false)
261
+ changes = resolve(candidates)
262
+ return restamps + (adopt? ? with_adoptions(changes) : changes) unless commit
263
+
264
+ changes = with_adoptions(changes, create: true) if adopt?
265
+ changes = restamps + changes
266
+ applicable = changes.select(&:applicable?)
267
+ UI.stepping(applicable, "Retitling") { |c| github.retitle(number: c.number, title: c.new_title) }
268
+ changes
269
+ end
270
+
271
+ # @return [Array<OpenLanes::Lifecycle::Adoption>] the adopter, memoized
272
+ def adoption = @adoption ||= Adoption.new(tree:, github:, root: @root)
273
+
274
+ private
275
+
276
+ # @return [Array<Hash>] pull requests with no prefix yet
277
+ def candidates
278
+ github.pulls.reject { |pr| pr[:title].to_s.match?(PREFIXED) || pr[:title].to_s.match?(STALE) }
279
+ end
280
+
281
+ # @return [Array<Hash>] pull requests still wearing the old marker
282
+ def stale = github.pulls.select { |pr| pr[:title].to_s.match?(STALE) }
283
+
284
+ # @param pull [Hash]
285
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
286
+ def restamped(pull)
287
+ Change.new(number: pull[:number],
288
+ title: pull[:title],
289
+ ordinal: nil,
290
+ ambiguous: false,
291
+ new_title: pull[:title].sub(STALE, "[#{OpenLanes::NO_PLAN_PREFIX}] "),
292
+ reason: "the no-plan marker is now [#{OpenLanes::NO_PLAN_PREFIX}]",
293
+ assumed: false,
294
+ adopted: false)
295
+ end
296
+
297
+ # @param pulls [Array<Hash>]
298
+ # @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
299
+ def resolve(pulls) = pulls.map { |pr| change_for(pr) }
300
+
301
+ # Replace every flagged change with one that points at a plan folder the
302
+ # pull request now owns. The unresolvable ones were unresolvable because
303
+ # no plan described them — so the answer is a plan, not a shrug.
304
+ #
305
+ # @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
306
+ # @param create [Boolean] mint the folders, rather than only saying so
307
+ # @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
308
+ def with_adoptions(changes, create: false)
309
+ orphans = changes.select(&:ambiguous?)
310
+ return changes if orphans.empty?
311
+
312
+ pulls = orphans.map { |c| pull_by_number.fetch(c.number) }
313
+ adoptees = create ? adoption.call(pulls) : adoption.plan(pulls)
314
+ by_number = adoptees.to_h { |a| [a.pull[:number], a] }
315
+
316
+ changes.map { |c| c.ambiguous? && by_number[c.number] ? adopted(c, by_number[c.number]) : c }
317
+ end
318
+
319
+ # @return [Hash{Integer => Hash}]
320
+ def pull_by_number = @pull_by_number ||= candidates.to_h { |pr| [pr[:number], pr] }
321
+
322
+ # @param change [OpenLanes::Lifecycle::Resync::Prs::Change]
323
+ # @param adoptee [OpenLanes::Lifecycle::Adoption::Adoptee]
324
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
325
+ def adopted(change, adoptee)
326
+ change.with(
327
+ ordinal: adoptee.ordinal,
328
+ new_title: "#{adoptee.ordinal.to_prefix} #{change.title}",
329
+ reason: "#{change.reason} — adopted into #{adoptee.dirname}",
330
+ ambiguous: false,
331
+ adopted: true
332
+ )
333
+ end
334
+
335
+ # @param pull [Hash]
336
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
337
+ def change_for(pull)
338
+ from_branch(pull) || from_files(pull) || no_plan(pull)
339
+ end
340
+
341
+ # 1. The branch name — the one moment the author certainly knew.
342
+ #
343
+ # @param pull [Hash]
344
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change, nil]
345
+ def from_branch(pull)
346
+ match = BRANCH_PATTERN.match(pull[:branch].to_s) or return nil
347
+ ordinal = Plans::Ordinal.parse(match[1])
348
+
349
+ unless tree.include?(ordinal)
350
+ return flag(pull, "branch names #{ordinal}, which has no folder in #{File.basename(tree.dir)}")
351
+ end
352
+
353
+ resolved(pull, ordinal, "branch #{pull[:branch]}")
354
+ end
355
+
356
+ # 2. The diff, and only when it touches exactly one plan.
357
+ #
358
+ # @param pull [Hash]
359
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change, nil]
360
+ def from_files(pull)
361
+ touched = Array(pull[:files]).filter_map { |path| ordinal_for_path(path) }.uniq
362
+ case touched.size
363
+ when 1 then resolved(pull, touched.first, "diff touches only #{touched.first}")
364
+ when 0 then nil
365
+ else flag(pull, "diff touches #{touched.join(", ")} and none is obviously primary")
366
+ end
367
+ end
368
+
369
+ # @param path [String] a path from the diff
370
+ # @return [OpenLanes::Plans::Ordinal, nil]
371
+ def ordinal_for_path(path)
372
+ parts = path.to_s.split("/")
373
+ index = parts.index(OpenLanes::PLANS_DIR) or return nil
374
+ folder = parts[index + 1] or return nil
375
+ return nil if parts.length <= index + 2 # a file directly under .plans belongs to no plan
376
+
377
+ ordinal = Plans::Ordinal.from_dirname(folder)
378
+ ordinal if ordinal && tree.include?(ordinal)
379
+ end
380
+
381
+ # @param pull [Hash]
382
+ # @param ordinal [OpenLanes::Plans::Ordinal]
383
+ # @param why [String]
384
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
385
+ def resolved(pull, ordinal, why)
386
+ Change.new(number: pull[:number],
387
+ title: pull[:title],
388
+ ordinal:,
389
+ reason: why,
390
+ new_title: "#{ordinal.to_prefix} #{pull[:title]}",
391
+ ambiguous: false,
392
+ assumed: false,
393
+ adopted: false)
394
+ end
395
+
396
+ # Nothing resolved, so this is developer work — but that is an assertion
397
+ # about intent, so it is marked as assumed rather than stated as fact.
398
+ #
399
+ # @param pull [Hash]
400
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
401
+ def no_plan(pull)
402
+ Change.new(number: pull[:number],
403
+ title: pull[:title],
404
+ ordinal: nil,
405
+ new_title: "[#{OpenLanes::NO_PLAN_PREFIX}] #{pull[:title]}",
406
+ reason: "no plan resolved from the branch or the diff",
407
+ ambiguous: false,
408
+ assumed: true,
409
+ adopted: false)
410
+ end
411
+
412
+ # @param pull [Hash]
413
+ # @param why [String]
414
+ # @return [OpenLanes::Lifecycle::Resync::Prs::Change]
415
+ def flag(pull, why)
416
+ Change.new(number: pull[:number],
417
+ title: pull[:title],
418
+ new_title: nil,
419
+ ordinal: nil,
420
+ reason: why,
421
+ ambiguous: true,
422
+ assumed: false,
423
+ adopted: false)
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end