pr-merge-queue 0.0.0-stage → 0.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 (331) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +125 -2
  3. package/dist/claim-pr/claim-pr.d.mts +45 -0
  4. package/dist/claim-pr/claim-pr.d.mts.map +1 -0
  5. package/dist/claim-pr/claim-pr.mjs +217 -0
  6. package/dist/claim-pr/claim-pr.mjs.map +1 -0
  7. package/dist/claim-pr/index.d.mts +2 -0
  8. package/dist/claim-pr/index.d.mts.map +1 -0
  9. package/dist/claim-pr/index.mjs +2 -0
  10. package/dist/claim-pr/index.mjs.map +1 -0
  11. package/dist/cmd/claim-pr.d.mts +3 -0
  12. package/dist/cmd/claim-pr.d.mts.map +1 -0
  13. package/dist/cmd/claim-pr.mjs +20 -0
  14. package/dist/cmd/claim-pr.mjs.map +1 -0
  15. package/dist/cmd/open-pr.d.mts +3 -0
  16. package/dist/cmd/open-pr.d.mts.map +1 -0
  17. package/dist/cmd/open-pr.mjs +20 -0
  18. package/dist/cmd/open-pr.mjs.map +1 -0
  19. package/dist/cmd/pr-report.d.mts +3 -0
  20. package/dist/cmd/pr-report.d.mts.map +1 -0
  21. package/dist/cmd/pr-report.mjs +20 -0
  22. package/dist/cmd/pr-report.mjs.map +1 -0
  23. package/dist/cmd/push-fixed-pr.d.mts +3 -0
  24. package/dist/cmd/push-fixed-pr.d.mts.map +1 -0
  25. package/dist/cmd/push-fixed-pr.mjs +20 -0
  26. package/dist/cmd/push-fixed-pr.mjs.map +1 -0
  27. package/dist/cmd/report-unowned-prs.d.mts +3 -0
  28. package/dist/cmd/report-unowned-prs.d.mts.map +1 -0
  29. package/dist/cmd/report-unowned-prs.mjs +20 -0
  30. package/dist/cmd/report-unowned-prs.mjs.map +1 -0
  31. package/dist/cmd/unblock-prs.d.mts +3 -0
  32. package/dist/cmd/unblock-prs.d.mts.map +1 -0
  33. package/dist/cmd/unblock-prs.mjs +19 -0
  34. package/dist/cmd/unblock-prs.mjs.map +1 -0
  35. package/dist/entry-point.d.mts +3 -0
  36. package/dist/entry-point.d.mts.map +1 -0
  37. package/dist/entry-point.mjs +3 -0
  38. package/dist/entry-point.mjs.map +1 -0
  39. package/dist/lib/cli-args.d.mts +27 -0
  40. package/dist/lib/cli-args.d.mts.map +1 -0
  41. package/dist/lib/cli-args.mjs +55 -0
  42. package/dist/lib/cli-args.mjs.map +1 -0
  43. package/dist/lib/github-api.d.mts +153 -0
  44. package/dist/lib/github-api.d.mts.map +1 -0
  45. package/dist/lib/github-api.mjs +291 -0
  46. package/dist/lib/github-api.mjs.map +1 -0
  47. package/dist/lib/index.d.mts +7 -0
  48. package/dist/lib/index.d.mts.map +1 -0
  49. package/dist/lib/index.mjs +7 -0
  50. package/dist/lib/index.mjs.map +1 -0
  51. package/dist/lib/json.d.mts +12 -0
  52. package/dist/lib/json.d.mts.map +1 -0
  53. package/dist/lib/json.mjs +28 -0
  54. package/dist/lib/json.mjs.map +1 -0
  55. package/dist/lib/log.d.mts +10 -0
  56. package/dist/lib/log.d.mts.map +1 -0
  57. package/dist/lib/log.mjs +13 -0
  58. package/dist/lib/log.mjs.map +1 -0
  59. package/dist/lib/shell.d.mts +18 -0
  60. package/dist/lib/shell.d.mts.map +1 -0
  61. package/dist/lib/shell.mjs +42 -0
  62. package/dist/lib/shell.mjs.map +1 -0
  63. package/dist/lib/time.d.mts +17 -0
  64. package/dist/lib/time.d.mts.map +1 -0
  65. package/dist/lib/time.mjs +35 -0
  66. package/dist/lib/time.mjs.map +1 -0
  67. package/dist/open-pr/github.d.mts +72 -0
  68. package/dist/open-pr/github.d.mts.map +1 -0
  69. package/dist/open-pr/github.mjs +249 -0
  70. package/dist/open-pr/github.mjs.map +1 -0
  71. package/dist/open-pr/index.d.mts +6 -0
  72. package/dist/open-pr/index.d.mts.map +1 -0
  73. package/dist/open-pr/index.mjs +6 -0
  74. package/dist/open-pr/index.mjs.map +1 -0
  75. package/dist/open-pr/main.d.mts +36 -0
  76. package/dist/open-pr/main.d.mts.map +1 -0
  77. package/dist/open-pr/main.mjs +204 -0
  78. package/dist/open-pr/main.mjs.map +1 -0
  79. package/dist/open-pr/options.d.mts +20 -0
  80. package/dist/open-pr/options.d.mts.map +1 -0
  81. package/dist/open-pr/options.mjs +66 -0
  82. package/dist/open-pr/options.mjs.map +1 -0
  83. package/dist/open-pr/steps.d.mts +23 -0
  84. package/dist/open-pr/steps.d.mts.map +1 -0
  85. package/dist/open-pr/steps.mjs +38 -0
  86. package/dist/open-pr/steps.mjs.map +1 -0
  87. package/dist/open-pr/types.d.mts +179 -0
  88. package/dist/open-pr/types.d.mts.map +1 -0
  89. package/dist/open-pr/types.mjs +20 -0
  90. package/dist/open-pr/types.mjs.map +1 -0
  91. package/dist/pr-report/github.d.mts +59 -0
  92. package/dist/pr-report/github.d.mts.map +1 -0
  93. package/dist/pr-report/github.mjs +561 -0
  94. package/dist/pr-report/github.mjs.map +1 -0
  95. package/dist/pr-report/index.d.mts +8 -0
  96. package/dist/pr-report/index.d.mts.map +1 -0
  97. package/dist/pr-report/index.mjs +8 -0
  98. package/dist/pr-report/index.mjs.map +1 -0
  99. package/dist/pr-report/linked-issues.d.mts +15 -0
  100. package/dist/pr-report/linked-issues.d.mts.map +1 -0
  101. package/dist/pr-report/linked-issues.mjs +39 -0
  102. package/dist/pr-report/linked-issues.mjs.map +1 -0
  103. package/dist/pr-report/main.d.mts +42 -0
  104. package/dist/pr-report/main.d.mts.map +1 -0
  105. package/dist/pr-report/main.mjs +163 -0
  106. package/dist/pr-report/main.mjs.map +1 -0
  107. package/dist/pr-report/options.d.mts +36 -0
  108. package/dist/pr-report/options.d.mts.map +1 -0
  109. package/dist/pr-report/options.mjs +90 -0
  110. package/dist/pr-report/options.mjs.map +1 -0
  111. package/dist/pr-report/queue.d.mts +28 -0
  112. package/dist/pr-report/queue.d.mts.map +1 -0
  113. package/dist/pr-report/queue.mjs +74 -0
  114. package/dist/pr-report/queue.mjs.map +1 -0
  115. package/dist/pr-report/render.d.mts +14 -0
  116. package/dist/pr-report/render.d.mts.map +1 -0
  117. package/dist/pr-report/render.mjs +318 -0
  118. package/dist/pr-report/render.mjs.map +1 -0
  119. package/dist/pr-report/repo-settings.d.mts +12 -0
  120. package/dist/pr-report/repo-settings.d.mts.map +1 -0
  121. package/dist/pr-report/repo-settings.mjs +22 -0
  122. package/dist/pr-report/repo-settings.mjs.map +1 -0
  123. package/dist/push-fixed-pr/index.d.mts +2 -0
  124. package/dist/push-fixed-pr/index.d.mts.map +1 -0
  125. package/dist/push-fixed-pr/index.mjs +2 -0
  126. package/dist/push-fixed-pr/index.mjs.map +1 -0
  127. package/dist/push-fixed-pr/push-fixed-pr.d.mts +54 -0
  128. package/dist/push-fixed-pr/push-fixed-pr.d.mts.map +1 -0
  129. package/dist/push-fixed-pr/push-fixed-pr.mjs +172 -0
  130. package/dist/push-fixed-pr/push-fixed-pr.mjs.map +1 -0
  131. package/dist/report-unowned-prs/index.d.mts +2 -0
  132. package/dist/report-unowned-prs/index.d.mts.map +1 -0
  133. package/dist/report-unowned-prs/index.mjs +2 -0
  134. package/dist/report-unowned-prs/index.mjs.map +1 -0
  135. package/dist/report-unowned-prs/report-unowned-prs.d.mts +49 -0
  136. package/dist/report-unowned-prs/report-unowned-prs.d.mts.map +1 -0
  137. package/dist/report-unowned-prs/report-unowned-prs.mjs +127 -0
  138. package/dist/report-unowned-prs/report-unowned-prs.mjs.map +1 -0
  139. package/dist/tsconfig.json +1 -0
  140. package/dist/unblock-prs/auto-fix.d.mts +45 -0
  141. package/dist/unblock-prs/auto-fix.d.mts.map +1 -0
  142. package/dist/unblock-prs/auto-fix.mjs +207 -0
  143. package/dist/unblock-prs/auto-fix.mjs.map +1 -0
  144. package/dist/unblock-prs/checks.d.mts +307 -0
  145. package/dist/unblock-prs/checks.d.mts.map +1 -0
  146. package/dist/unblock-prs/checks.mjs +372 -0
  147. package/dist/unblock-prs/checks.mjs.map +1 -0
  148. package/dist/unblock-prs/claims.d.mts +21 -0
  149. package/dist/unblock-prs/claims.d.mts.map +1 -0
  150. package/dist/unblock-prs/claims.mjs +23 -0
  151. package/dist/unblock-prs/claims.mjs.map +1 -0
  152. package/dist/unblock-prs/config.d.mts +487 -0
  153. package/dist/unblock-prs/config.d.mts.map +1 -0
  154. package/dist/unblock-prs/config.mjs +119 -0
  155. package/dist/unblock-prs/config.mjs.map +1 -0
  156. package/dist/unblock-prs/constants.d.mts +28 -0
  157. package/dist/unblock-prs/constants.d.mts.map +1 -0
  158. package/dist/unblock-prs/constants.mjs +28 -0
  159. package/dist/unblock-prs/constants.mjs.map +1 -0
  160. package/dist/unblock-prs/demotions.d.mts +42 -0
  161. package/dist/unblock-prs/demotions.d.mts.map +1 -0
  162. package/dist/unblock-prs/demotions.mjs +50 -0
  163. package/dist/unblock-prs/demotions.mjs.map +1 -0
  164. package/dist/unblock-prs/github.d.mts +739 -0
  165. package/dist/unblock-prs/github.d.mts.map +1 -0
  166. package/dist/unblock-prs/github.mjs +626 -0
  167. package/dist/unblock-prs/github.mjs.map +1 -0
  168. package/dist/unblock-prs/index.d.mts +25 -0
  169. package/dist/unblock-prs/index.d.mts.map +1 -0
  170. package/dist/unblock-prs/index.mjs +25 -0
  171. package/dist/unblock-prs/index.mjs.map +1 -0
  172. package/dist/unblock-prs/labels.d.mts +49 -0
  173. package/dist/unblock-prs/labels.d.mts.map +1 -0
  174. package/dist/unblock-prs/labels.mjs +49 -0
  175. package/dist/unblock-prs/labels.mjs.map +1 -0
  176. package/dist/unblock-prs/main.d.mts +179 -0
  177. package/dist/unblock-prs/main.d.mts.map +1 -0
  178. package/dist/unblock-prs/main.mjs +640 -0
  179. package/dist/unblock-prs/main.mjs.map +1 -0
  180. package/dist/unblock-prs/merge-after.d.mts +46 -0
  181. package/dist/unblock-prs/merge-after.d.mts.map +1 -0
  182. package/dist/unblock-prs/merge-after.mjs +65 -0
  183. package/dist/unblock-prs/merge-after.mjs.map +1 -0
  184. package/dist/unblock-prs/options.d.mts +35 -0
  185. package/dist/unblock-prs/options.d.mts.map +1 -0
  186. package/dist/unblock-prs/options.mjs +88 -0
  187. package/dist/unblock-prs/options.mjs.map +1 -0
  188. package/dist/unblock-prs/owner.d.mts +32 -0
  189. package/dist/unblock-prs/owner.d.mts.map +1 -0
  190. package/dist/unblock-prs/owner.mjs +63 -0
  191. package/dist/unblock-prs/owner.mjs.map +1 -0
  192. package/dist/unblock-prs/quiet.d.mts +21 -0
  193. package/dist/unblock-prs/quiet.d.mts.map +1 -0
  194. package/dist/unblock-prs/quiet.mjs +42 -0
  195. package/dist/unblock-prs/quiet.mjs.map +1 -0
  196. package/dist/unblock-prs/rebase.d.mts +120 -0
  197. package/dist/unblock-prs/rebase.d.mts.map +1 -0
  198. package/dist/unblock-prs/rebase.mjs +576 -0
  199. package/dist/unblock-prs/rebase.mjs.map +1 -0
  200. package/dist/unblock-prs/release.d.mts +62 -0
  201. package/dist/unblock-prs/release.d.mts.map +1 -0
  202. package/dist/unblock-prs/release.mjs +94 -0
  203. package/dist/unblock-prs/release.mjs.map +1 -0
  204. package/dist/unblock-prs/set-aside-comment.d.mts +20 -0
  205. package/dist/unblock-prs/set-aside-comment.d.mts.map +1 -0
  206. package/dist/unblock-prs/set-aside-comment.mjs +93 -0
  207. package/dist/unblock-prs/set-aside-comment.mjs.map +1 -0
  208. package/dist/unblock-prs/set-aside-detail.d.mts +41 -0
  209. package/dist/unblock-prs/set-aside-detail.d.mts.map +1 -0
  210. package/dist/unblock-prs/set-aside-detail.mjs +56 -0
  211. package/dist/unblock-prs/set-aside-detail.mjs.map +1 -0
  212. package/dist/unblock-prs/skips.d.mts +85 -0
  213. package/dist/unblock-prs/skips.d.mts.map +1 -0
  214. package/dist/unblock-prs/skips.mjs +141 -0
  215. package/dist/unblock-prs/skips.mjs.map +1 -0
  216. package/dist/unblock-prs/stack.d.mts +98 -0
  217. package/dist/unblock-prs/stack.d.mts.map +1 -0
  218. package/dist/unblock-prs/stack.mjs +141 -0
  219. package/dist/unblock-prs/stack.mjs.map +1 -0
  220. package/dist/unblock-prs/triage.d.mts +28 -0
  221. package/dist/unblock-prs/triage.d.mts.map +1 -0
  222. package/dist/unblock-prs/triage.mjs +396 -0
  223. package/dist/unblock-prs/triage.mjs.map +1 -0
  224. package/dist/unblock-prs/types.d.mts +793 -0
  225. package/dist/unblock-prs/types.d.mts.map +1 -0
  226. package/dist/unblock-prs/types.mjs +26 -0
  227. package/dist/unblock-prs/types.mjs.map +1 -0
  228. package/dist/unblock-prs/util.d.mts +23 -0
  229. package/dist/unblock-prs/util.d.mts.map +1 -0
  230. package/dist/unblock-prs/util.mjs +60 -0
  231. package/dist/unblock-prs/util.mjs.map +1 -0
  232. package/dist/unblock-prs/version-pr.d.mts +57 -0
  233. package/dist/unblock-prs/version-pr.d.mts.map +1 -0
  234. package/dist/unblock-prs/version-pr.mjs +79 -0
  235. package/dist/unblock-prs/version-pr.mjs.map +1 -0
  236. package/dist/unblock-prs/watch.d.mts +17 -0
  237. package/dist/unblock-prs/watch.d.mts.map +1 -0
  238. package/dist/unblock-prs/watch.mjs +128 -0
  239. package/dist/unblock-prs/watch.mjs.map +1 -0
  240. package/package.json +118 -3
  241. package/src/claim-pr/claim-pr.mts +380 -0
  242. package/src/claim-pr/claim-pr.test.mts +176 -0
  243. package/src/claim-pr/index.mts +1 -0
  244. package/src/cmd/claim-pr.mts +32 -0
  245. package/src/cmd/open-pr.mts +28 -0
  246. package/src/cmd/pr-report.mts +28 -0
  247. package/src/cmd/push-fixed-pr.mts +36 -0
  248. package/src/cmd/report-unowned-prs.mts +37 -0
  249. package/src/cmd/unblock-prs.mts +24 -0
  250. package/src/entry-point.mts +3 -0
  251. package/src/lib/cli-args.mts +86 -0
  252. package/src/lib/cli-args.test.mts +92 -0
  253. package/src/lib/github-api.mts +463 -0
  254. package/src/lib/github-api.test.mts +40 -0
  255. package/src/lib/index.mts +6 -0
  256. package/src/lib/json.mts +43 -0
  257. package/src/lib/json.test.mts +34 -0
  258. package/src/lib/log.mts +19 -0
  259. package/src/lib/log.test.mts +45 -0
  260. package/src/lib/shell.mts +52 -0
  261. package/src/lib/time.mts +49 -0
  262. package/src/open-pr/README.md +237 -0
  263. package/src/open-pr/github.mts +444 -0
  264. package/src/open-pr/index.mts +5 -0
  265. package/src/open-pr/main.mts +327 -0
  266. package/src/open-pr/options.mts +101 -0
  267. package/src/open-pr/options.test.mts +67 -0
  268. package/src/open-pr/steps.mts +57 -0
  269. package/src/open-pr/steps.test.mts +59 -0
  270. package/src/open-pr/types.mts +42 -0
  271. package/src/pr-report/README.md +244 -0
  272. package/src/pr-report/github.mts +911 -0
  273. package/src/pr-report/index.mts +7 -0
  274. package/src/pr-report/linked-issues.mts +51 -0
  275. package/src/pr-report/linked-issues.test.mts +65 -0
  276. package/src/pr-report/main.mts +257 -0
  277. package/src/pr-report/options.mts +140 -0
  278. package/src/pr-report/queue.mts +125 -0
  279. package/src/pr-report/render.mts +428 -0
  280. package/src/pr-report/render.test.mts +379 -0
  281. package/src/pr-report/repo-settings.mts +34 -0
  282. package/src/push-fixed-pr/index.mts +1 -0
  283. package/src/push-fixed-pr/push-fixed-pr.mts +276 -0
  284. package/src/push-fixed-pr/push-fixed-pr.test.mts +228 -0
  285. package/src/report-unowned-prs/index.mts +1 -0
  286. package/src/report-unowned-prs/report-unowned-prs.mts +190 -0
  287. package/src/report-unowned-prs/report-unowned-prs.test.mts +87 -0
  288. package/src/unblock-prs/README.md +1616 -0
  289. package/src/unblock-prs/auto-fix.mts +334 -0
  290. package/src/unblock-prs/auto-fix.test.mts +159 -0
  291. package/src/unblock-prs/checks.mts +538 -0
  292. package/src/unblock-prs/checks.test.mts +507 -0
  293. package/src/unblock-prs/claims.mts +33 -0
  294. package/src/unblock-prs/claims.test.mts +64 -0
  295. package/src/unblock-prs/config.mts +187 -0
  296. package/src/unblock-prs/config.schema.json +71 -0
  297. package/src/unblock-prs/config.test.mts +139 -0
  298. package/src/unblock-prs/constants.mts +33 -0
  299. package/src/unblock-prs/demotions.mts +73 -0
  300. package/src/unblock-prs/demotions.test.mts +100 -0
  301. package/src/unblock-prs/github.mts +1042 -0
  302. package/src/unblock-prs/github.test.mts +84 -0
  303. package/src/unblock-prs/index.mts +24 -0
  304. package/src/unblock-prs/labels.mts +70 -0
  305. package/src/unblock-prs/main.mts +1051 -0
  306. package/src/unblock-prs/merge-after.mts +117 -0
  307. package/src/unblock-prs/merge-after.test.mts +120 -0
  308. package/src/unblock-prs/options.mts +157 -0
  309. package/src/unblock-prs/owner.mts +91 -0
  310. package/src/unblock-prs/owner.test.mts +114 -0
  311. package/src/unblock-prs/quiet.mts +62 -0
  312. package/src/unblock-prs/quiet.test.mts +175 -0
  313. package/src/unblock-prs/rebase.mts +962 -0
  314. package/src/unblock-prs/release.mts +149 -0
  315. package/src/unblock-prs/release.test.mts +209 -0
  316. package/src/unblock-prs/set-aside-comment.mts +143 -0
  317. package/src/unblock-prs/set-aside-comment.test.mts +148 -0
  318. package/src/unblock-prs/set-aside-detail.mts +82 -0
  319. package/src/unblock-prs/set-aside-detail.test.mts +131 -0
  320. package/src/unblock-prs/skips.mts +215 -0
  321. package/src/unblock-prs/skips.test.mts +280 -0
  322. package/src/unblock-prs/stack.mts +220 -0
  323. package/src/unblock-prs/stack.test.mts +449 -0
  324. package/src/unblock-prs/triage.mts +669 -0
  325. package/src/unblock-prs/triage.test.mts +100 -0
  326. package/src/unblock-prs/types.mts +358 -0
  327. package/src/unblock-prs/util.mts +78 -0
  328. package/src/unblock-prs/util.test.mts +26 -0
  329. package/src/unblock-prs/version-pr.mts +112 -0
  330. package/src/unblock-prs/version-pr.test.mts +78 -0
  331. package/src/unblock-prs/watch.mts +219 -0
@@ -0,0 +1,428 @@
1
+ /** The report as text: Markdown for GitHub and Claude, plain for a terminal. */
2
+
3
+ import {
4
+ summarize,
5
+ type ChecksSummary,
6
+ type IssueStartState,
7
+ type PrReport,
8
+ type ReferenceStatus,
9
+ type ReportEntry,
10
+ type ReportIssue,
11
+ type TreeNode,
12
+ } from 'pr-report-core';
13
+ import { Arr } from 'ts-data-forge';
14
+ import type { ReadonlyRecord } from 'ts-type-forge';
15
+
16
+ const EMPTY = 'No open pull requests.';
17
+
18
+ /** What each verdict looks like, in a document and in a terminal. */
19
+ const GLYPH = {
20
+ failing: { markdown: '❌', terminal: '✗' },
21
+ passed: { markdown: '✅', terminal: '✓' },
22
+ paused: { markdown: '⏸️', terminal: '⏸' },
23
+ pending: { markdown: '🟡', terminal: '•' },
24
+ } as const satisfies ReadonlyRecord<
25
+ ChecksSummary['verdict'],
26
+ Readonly<{ markdown: string; terminal: string }>
27
+ >;
28
+
29
+ /**
30
+ * The report as Markdown, for pasting into GitHub or a conversation with
31
+ * Claude.
32
+ *
33
+ * The merge order is a nested list rather than a table because the nesting
34
+ * *is* the information — a table would have to spell the tree back out in a
35
+ * column, and a reader would have to rebuild it.
36
+ */
37
+ export const renderMarkdown = (report: PrReport): string => {
38
+ if (!Arr.isNonEmpty(report.entries)) {
39
+ return [
40
+ heading(report),
41
+ '',
42
+ EMPTY,
43
+ ...mergedSection(report, 'markdown'),
44
+ ...issuesSection(report, 'markdown'),
45
+ '',
46
+ ].join('\n');
47
+ }
48
+
49
+ const byNumber = index(report);
50
+
51
+ const line = (node: TreeNode, depth: number): readonly string[] => {
52
+ const entry = byNumber.get(node.number);
53
+
54
+ if (entry === undefined) {
55
+ return [];
56
+ }
57
+
58
+ const indent = ' '.repeat(depth);
59
+
60
+ const body = node.repeated
61
+ ? (`[#${node.number}](${entry.url}) — see above` as const)
62
+ : describe(entry, 'markdown');
63
+
64
+ return Arr.toUnshifted(`${indent}- ${body}`)(
65
+ node.children.flatMap((child) => line(child, depth + 1)),
66
+ );
67
+ };
68
+
69
+ return [
70
+ heading(report),
71
+ '',
72
+ summary(report),
73
+ '',
74
+ '## Merge order',
75
+ '',
76
+ ...report.roots.flatMap((root) => line(root, 0)),
77
+ ...cyclesSection(report),
78
+ ...mergedSection(report, 'markdown'),
79
+ ...issuesSection(report, 'markdown'),
80
+ ...footnote(report),
81
+ '',
82
+ ].join('\n');
83
+ };
84
+
85
+ /** The same report drawn with box characters, for a terminal. */
86
+ export const renderTerminal = (report: PrReport): string => {
87
+ if (!Arr.isNonEmpty(report.entries)) {
88
+ return [
89
+ title(report),
90
+ '',
91
+ EMPTY,
92
+ ...mergedSection(report, 'terminal').map(withoutHeadingMark),
93
+ ...issuesSection(report, 'terminal').map(withoutHeadingMark),
94
+ '',
95
+ ].join('\n');
96
+ }
97
+
98
+ const byNumber = index(report);
99
+
100
+ const line = (
101
+ node: TreeNode,
102
+ prefix: string,
103
+ connector: string,
104
+ ): readonly string[] => {
105
+ const entry = byNumber.get(node.number);
106
+
107
+ if (entry === undefined) {
108
+ return [];
109
+ }
110
+
111
+ const body = node.repeated
112
+ ? (`#${node.number} — see above` as const)
113
+ : describe(entry, 'terminal');
114
+
115
+ const childPrefix =
116
+ connector === ''
117
+ ? prefix
118
+ : (`${prefix}${connector === '└─ ' ? ' '.repeat(3) : '│ '}` as const);
119
+
120
+ return Arr.toUnshifted(`${prefix}${connector}${body}`)(
121
+ node.children.flatMap((child, i) =>
122
+ line(
123
+ child,
124
+ childPrefix,
125
+ i === node.children.length - 1 ? '└─ ' : '├─ ',
126
+ ),
127
+ ),
128
+ );
129
+ };
130
+
131
+ return [
132
+ title(report),
133
+ summary(report).replaceAll('*', ''),
134
+ '',
135
+ ...report.roots.flatMap((root) => line(root, '', '')),
136
+ ...cyclesSection(report).map(withoutHeadingMark),
137
+ ...mergedSection(report, 'terminal').map(withoutHeadingMark),
138
+ ...issuesSection(report, 'terminal').map(withoutHeadingMark),
139
+ ...footnote(report).map((l) => l.replace(/^> /u, '')),
140
+ '',
141
+ ].join('\n');
142
+ };
143
+
144
+ /** A Markdown heading as the plain line a terminal shows. */
145
+ const withoutHeadingMark = (line: string): string => line.replace(/^#+ /u, '');
146
+
147
+ const index = (report: PrReport): ReadonlyMap<number, ReportEntry> =>
148
+ new Map(report.entries.map((entry) => [entry.number, entry]));
149
+
150
+ const title = (report: PrReport): string =>
151
+ `Open pull requests — ${report.repo.owner}/${report.repo.name}` as const;
152
+
153
+ const heading = (report: PrReport): string => `# ${title(report)}` as const;
154
+
155
+ /**
156
+ * One line saying how much there is and how much of it wants attention, so
157
+ * that a reader who opens the report and closes it again has still learnt
158
+ * the only thing a daily report has to tell them. The Pull Requests
159
+ * Manager page leads with the same counts, from the same {@link summarize}.
160
+ */
161
+ const summary = (report: PrReport): string => {
162
+ const counts = summarize(report.entries);
163
+
164
+ return [
165
+ `**${counts.open} open**`,
166
+ `${counts.queued} queued`,
167
+ `${counts.draft} draft`,
168
+ `${counts.failing} failing`,
169
+ `${counts.behind} behind`,
170
+ `generated ${report.generatedAt}`,
171
+ ].join(' · ');
172
+ };
173
+
174
+ /** One pull request on one line: what it is, and what is holding it up. */
175
+ const describe = (
176
+ entry: ReportEntry,
177
+ format: 'markdown' | 'terminal',
178
+ ): string => {
179
+ const markdown = format === 'markdown';
180
+
181
+ const ref = markdown
182
+ ? (`[#${entry.number}](${entry.url})` as const)
183
+ : (`#${entry.number}` as const);
184
+
185
+ const glyph = GLYPH[entry.checks.verdict][format];
186
+
187
+ const detail = [
188
+ entry.isDraft ? 'draft' : undefined,
189
+ entry.stackedOn === undefined
190
+ ? undefined
191
+ : (`stacked on #${entry.stackedOn}` as const),
192
+ ...inheritedOrder(entry),
193
+ commits(entry),
194
+ ...entry.labels.map(({ name }) => (markdown ? `\`${name}\`` : `[${name}]`)),
195
+ autoMerge(entry),
196
+ issues(entry, markdown),
197
+ failures(entry.checks),
198
+ ].filter((part) => part !== undefined);
199
+
200
+ const title_ = markdown ? (`**${entry.title}**` as const) : entry.title;
201
+
202
+ return Arr.toUnshifted(`${glyph} ${ref} ${title_}`)(detail).join(' · ');
203
+ };
204
+
205
+ /**
206
+ * What the issues it closes add to its order (`inheritStartAfter`): the open
207
+ * pull requests it now merges after, which the tree already draws it under,
208
+ * and the open issues they name that nobody has started, which hold nothing.
209
+ */
210
+ const inheritedOrder = (entry: ReportEntry): readonly string[] =>
211
+ [
212
+ ...(Arr.isNonEmpty(entry.inheritedMergeAfter)
213
+ ? ([`after ${refs(entry.inheritedMergeAfter)} (Start-After)`] as const)
214
+ : ([] as const)),
215
+ ...(Arr.isNonEmpty(entry.notStartedIssues)
216
+ ? ([`${refs(entry.notStartedIssues)} (issue) not done`] as const)
217
+ : ([] as const)),
218
+ ] as const;
219
+
220
+ const refs = (numbers: readonly number[]): string =>
221
+ numbers.map((number) => `#${number}`).join(', ');
222
+
223
+ /**
224
+ * Whether anything will land the pull request once the checks go green, said
225
+ * only when something will. `unblock-prs` arms a queued pull request when it
226
+ * picks it, so one not yet armed — queued or not — is the ordinary case, and
227
+ * armed is the news: its turn has come.
228
+ */
229
+ const autoMerge = (entry: ReportEntry): string | undefined =>
230
+ entry.autoMerge ? 'auto-merge' : undefined;
231
+
232
+ /**
233
+ * `+3 / -12`: three commits of its own, twelve of its base it has not got.
234
+ * The second number is the one that matters — the ruleset blocks a branch
235
+ * that is behind `main`, and a stacked one behind the layer below it shows
236
+ * that layer's old commits in its diff — so anything but `-0` is a rebase
237
+ * waiting to happen.
238
+ */
239
+ const commits = (entry: ReportEntry): string =>
240
+ entry.comparison === undefined
241
+ ? 'ahead/behind unread'
242
+ : (`+${entry.comparison.aheadBy} / -${entry.comparison.behindBy}` as const);
243
+
244
+ const issues = (entry: ReportEntry, markdown: boolean): string | undefined =>
245
+ Arr.isNonEmpty(entry.linkedIssues)
246
+ ? (`closes ${entry.linkedIssues
247
+ .map((issue) =>
248
+ markdown ? `[#${issue.number}](${issue.url})` : `#${issue.number}`,
249
+ )
250
+ .join(', ')}` as const)
251
+ : undefined;
252
+
253
+ /**
254
+ * Which required contexts are red, because "failing" without the name of
255
+ * what failed is a second click for every reader of the report.
256
+ *
257
+ * While `skip-ci` is on, red is the normal state rather than news: the
258
+ * `opened` run is cancelled by the `labeled` run that supersedes it and
259
+ * leaves `*-result` checks red naming nothing, and no run since has been
260
+ * allowed to start. Reported as stale, so that a reader is not sent to open
261
+ * a run that was never about the current commit.
262
+ */
263
+ const failures = (checks: ChecksSummary): string | undefined => {
264
+ if (!Arr.isNonEmpty(checks.failed)) {
265
+ return checks.verdict === 'paused' ? 'checks paused' : undefined;
266
+ }
267
+
268
+ const names = checks.failed.join(', ');
269
+
270
+ return checks.verdict === 'paused'
271
+ ? `checks paused, stale red: ${names}`
272
+ : `failed: ${names}`;
273
+ };
274
+
275
+ const cyclesSection = (report: PrReport): readonly string[] =>
276
+ Arr.isNonEmpty(report.cycles)
277
+ ? ([
278
+ '',
279
+ '## Merge-After cycles',
280
+ '',
281
+ 'Each of these waits, in the end, for itself. Nothing can pick them up',
282
+ 'until one of the declarations goes.',
283
+ '',
284
+ ...report.cycles.map(
285
+ (cycle) =>
286
+ `- ${Arr.toPushed(cycle, cycle[0])
287
+ .map((n) => `#${n}`)
288
+ .join(' → ')}`,
289
+ ),
290
+ ] as const)
291
+ : ([] as const);
292
+
293
+ /**
294
+ * What landed, and when.
295
+ *
296
+ * Below the queue rather than above it, because the queue is what a reader
297
+ * can act on and this is what they no longer have to. Omitted entirely when
298
+ * nothing merged inside the window, rather than left as an empty heading
299
+ * saying the report looked.
300
+ */
301
+ const mergedSection = (
302
+ report: PrReport,
303
+ format: 'markdown' | 'terminal',
304
+ ): readonly string[] => {
305
+ if (!Arr.isNonEmpty(report.merged)) {
306
+ return [] as const;
307
+ }
308
+
309
+ const markdown = format === 'markdown';
310
+
311
+ return [
312
+ '',
313
+ `## Merged in the last ${report.mergedWithinDays} day${report.mergedWithinDays === 1 ? '' : 's'}`,
314
+ '',
315
+ ...report.merged.map((pr) => {
316
+ const ref = markdown
317
+ ? (`[#${pr.number}](${pr.url})` as const)
318
+ : (`#${pr.number}` as const);
319
+
320
+ const detail = [
321
+ pr.mergedAt,
322
+ `by ${pr.author}`,
323
+ ...pr.linkedIssues.map(({ number, url }) =>
324
+ markdown ? `closes [#${number}](${url})` : `closes #${number}`,
325
+ ),
326
+ ] as const;
327
+
328
+ return `- ${ref} ${markdown ? `**${pr.title}**` : pr.title} · ${detail.join(' · ')}`;
329
+ }),
330
+ ] as const;
331
+ };
332
+
333
+ /**
334
+ * What is open that is not a pull request.
335
+ *
336
+ * Last, after what merged, because it is the one section that is not about
337
+ * the queue at all: a reader asks "what is in flight" first and "what is
338
+ * waiting to be started" second. A list as long as its cap says `+`, since
339
+ * it may not be all of them.
340
+ */
341
+ const issuesSection = (
342
+ report: PrReport,
343
+ format: 'markdown' | 'terminal',
344
+ ): readonly string[] => {
345
+ if (!Arr.isNonEmpty(report.issues)) {
346
+ return [] as const;
347
+ }
348
+
349
+ const markdown = format === 'markdown';
350
+
351
+ const full = report.issues.length >= report.issuesLimit;
352
+
353
+ return [
354
+ '',
355
+ `## Open issues (${report.issues.length}${full ? '+' : ''})`,
356
+ '',
357
+ ...report.issues.map((issue) => {
358
+ const ref = markdown
359
+ ? (`[#${issue.number}](${issue.url})` as const)
360
+ : (`#${issue.number}` as const);
361
+
362
+ const detail = [
363
+ ...startDetail(issue),
364
+ `updated ${issue.updatedAt}`,
365
+ `by ${issue.author}`,
366
+ ...(issue.comments > 0
367
+ ? ([
368
+ `${issue.comments} comment${issue.comments === 1 ? '' : 's'}`,
369
+ ] as const)
370
+ : ([] as const)),
371
+ ...issue.labels.map(({ name }) =>
372
+ markdown ? `\`${name}\`` : `[${name}]`,
373
+ ),
374
+ ] as const;
375
+
376
+ return `- ${ref} ${markdown ? `**${issue.title}**` : issue.title} · ${detail.join(' · ')}`;
377
+ }),
378
+ ] as const;
379
+ };
380
+
381
+ /**
382
+ * How an issue stands for being started (`docs/issue-start-after.md`), unless
383
+ * it is free, which is the ordinary case; what its `Start-After:` names and
384
+ * where each stands; and a `Merge-After:` in its body, which is not read on
385
+ * an issue and is said so that it can be fixed.
386
+ */
387
+ const startDetail = (issue: ReportIssue): readonly string[] =>
388
+ [
389
+ ...(issue.start === 'free'
390
+ ? ([] as const)
391
+ : ([START_WORDS[issue.start]] as const)),
392
+ ...(Arr.isNonEmpty(issue.named)
393
+ ? ([
394
+ `after ${issue.named.map(({ number, status }) => `#${number} (${STATUS_WORDS[status]})`).join(', ')}`,
395
+ ] as const)
396
+ : ([] as const)),
397
+ ...(Arr.isNonEmpty(issue.misplacedMergeAfter)
398
+ ? ([
399
+ `Merge-After: ${refs(issue.misplacedMergeAfter)} is not read on an issue; use Start-After:`,
400
+ ] as const)
401
+ : ([] as const)),
402
+ ] as const;
403
+
404
+ const START_WORDS = {
405
+ free: 'free',
406
+ ready: 'ready',
407
+ 'in-progress': 'in progress',
408
+ attention: 'needs a look',
409
+ blocked: 'waiting',
410
+ cycle: 'on a Start-After cycle',
411
+ } as const satisfies ReadonlyRecord<IssueStartState, string>;
412
+
413
+ const STATUS_WORDS = {
414
+ waiting: 'open',
415
+ done: 'done',
416
+ abandoned: 'closed unfinished',
417
+ unknown: 'unknown',
418
+ } as const satisfies ReadonlyRecord<ReferenceStatus, string>;
419
+
420
+ /** Said once, at the bottom, rather than beside every pull request. */
421
+ const footnote = (report: PrReport): readonly string[] =>
422
+ report.authenticated
423
+ ? ([] as const)
424
+ : ([
425
+ '',
426
+ '> Read without a token: the linked issues are the ones the bodies',
427
+ "> declare with a closing keyword, not GitHub's own list.",
428
+ ] as const);