proctolog 0.1.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 (228) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +178 -0
  4. data/app/controllers/proctolog/acceptances_controller.rb +29 -0
  5. data/app/controllers/proctolog/application_controller.rb +32 -0
  6. data/app/controllers/proctolog/bursts_controller.rb +34 -0
  7. data/app/controllers/proctolog/choices_controller.rb +30 -0
  8. data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
  9. data/app/controllers/proctolog/decisions_controller.rb +33 -0
  10. data/app/controllers/proctolog/entries_controller.rb +14 -0
  11. data/app/controllers/proctolog/evidence_controller.rb +23 -0
  12. data/app/controllers/proctolog/freshness_controller.rb +27 -0
  13. data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
  14. data/app/controllers/proctolog/memberships_controller.rb +20 -0
  15. data/app/controllers/proctolog/mentions_controller.rb +16 -0
  16. data/app/controllers/proctolog/moments_controller.rb +26 -0
  17. data/app/controllers/proctolog/note_params.rb +24 -0
  18. data/app/controllers/proctolog/notes_controller.rb +56 -0
  19. data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
  20. data/app/controllers/proctolog/pictures_controller.rb +27 -0
  21. data/app/controllers/proctolog/prompts_controller.rb +28 -0
  22. data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
  23. data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
  24. data/app/controllers/proctolog/put_away_controller.rb +19 -0
  25. data/app/controllers/proctolog/recordings_controller.rb +28 -0
  26. data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
  27. data/app/controllers/proctolog/review_data_controller.rb +12 -0
  28. data/app/controllers/proctolog/rules_controller.rb +20 -0
  29. data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
  30. data/app/controllers/proctolog/scenario_scoped.rb +21 -0
  31. data/app/controllers/proctolog/scenarios_controller.rb +67 -0
  32. data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
  33. data/app/controllers/proctolog/waiting_controller.rb +14 -0
  34. data/app/controllers/proctolog/with_decisions.rb +28 -0
  35. data/app/helpers/proctolog/bursts_helper.rb +52 -0
  36. data/app/helpers/proctolog/decisions_helper.rb +102 -0
  37. data/app/helpers/proctolog/desk_helper.rb +61 -0
  38. data/app/helpers/proctolog/mentions_helper.rb +27 -0
  39. data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
  40. data/app/helpers/proctolog/prompts_helper.rb +21 -0
  41. data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
  42. data/app/helpers/proctolog/recordings_helper.rb +30 -0
  43. data/app/helpers/proctolog/rounds_helper.rb +27 -0
  44. data/app/helpers/proctolog/scenarios_helper.rb +123 -0
  45. data/app/helpers/proctolog/standings_helper.rb +80 -0
  46. data/app/jobs/proctolog/application_job.rb +8 -0
  47. data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
  48. data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
  49. data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
  50. data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
  51. data/app/models/proctolog/acceptance.rb +47 -0
  52. data/app/models/proctolog/app.rb +92 -0
  53. data/app/models/proctolog/browser.rb +42 -0
  54. data/app/models/proctolog/burst.rb +268 -0
  55. data/app/models/proctolog/burst_notices.rb +143 -0
  56. data/app/models/proctolog/chapters.rb +42 -0
  57. data/app/models/proctolog/check.rb +122 -0
  58. data/app/models/proctolog/choices.rb +112 -0
  59. data/app/models/proctolog/code_review.rb +251 -0
  60. data/app/models/proctolog/decision_record.rb +12 -0
  61. data/app/models/proctolog/decisions.rb +199 -0
  62. data/app/models/proctolog/description.rb +165 -0
  63. data/app/models/proctolog/difference.rb +138 -0
  64. data/app/models/proctolog/doctor.rb +127 -0
  65. data/app/models/proctolog/evidence.rb +29 -0
  66. data/app/models/proctolog/failure.rb +7 -0
  67. data/app/models/proctolog/focus.rb +48 -0
  68. data/app/models/proctolog/fresh_sitting.rb +35 -0
  69. data/app/models/proctolog/github.rb +353 -0
  70. data/app/models/proctolog/handover/brief.rb +138 -0
  71. data/app/models/proctolog/handover/layers.rb +128 -0
  72. data/app/models/proctolog/handover/places.rb +98 -0
  73. data/app/models/proctolog/handover/taken.rb +41 -0
  74. data/app/models/proctolog/handover/worklist.rb +35 -0
  75. data/app/models/proctolog/handover.rb +123 -0
  76. data/app/models/proctolog/housekeeping.rb +23 -0
  77. data/app/models/proctolog/kit.rb +36 -0
  78. data/app/models/proctolog/marks.rb +130 -0
  79. data/app/models/proctolog/membership.rb +91 -0
  80. data/app/models/proctolog/mentions.rb +117 -0
  81. data/app/models/proctolog/note.rb +94 -0
  82. data/app/models/proctolog/outside.rb +45 -0
  83. data/app/models/proctolog/overlay/brief.rb +52 -0
  84. data/app/models/proctolog/overlay/written.rb +33 -0
  85. data/app/models/proctolog/overlay.rb +87 -0
  86. data/app/models/proctolog/owner.rb +16 -0
  87. data/app/models/proctolog/pick.rb +47 -0
  88. data/app/models/proctolog/picture.rb +138 -0
  89. data/app/models/proctolog/pieces_rendering.rb +40 -0
  90. data/app/models/proctolog/project.rb +118 -0
  91. data/app/models/proctolog/pull_request.rb +89 -0
  92. data/app/models/proctolog/recording.rb +194 -0
  93. data/app/models/proctolog/release.rb +59 -0
  94. data/app/models/proctolog/rendering.rb +303 -0
  95. data/app/models/proctolog/reports.rb +66 -0
  96. data/app/models/proctolog/response_files.rb +55 -0
  97. data/app/models/proctolog/rounds.rb +305 -0
  98. data/app/models/proctolog/route.rb +151 -0
  99. data/app/models/proctolog/rules.rb +121 -0
  100. data/app/models/proctolog/scenario.rb +126 -0
  101. data/app/models/proctolog/server.rb +273 -0
  102. data/app/models/proctolog/since.rb +108 -0
  103. data/app/models/proctolog/size.rb +30 -0
  104. data/app/models/proctolog/standing.rb +132 -0
  105. data/app/models/proctolog/theme.rb +40 -0
  106. data/app/models/proctolog/unopened/numbers.rb +48 -0
  107. data/app/models/proctolog/unopened/opening.rb +88 -0
  108. data/app/models/proctolog/unopened.rb +203 -0
  109. data/app/models/proctolog/visit.rb +26 -0
  110. data/app/views/layouts/proctolog/application.html.erb +58 -0
  111. data/app/views/proctolog/acceptances/new.html.erb +34 -0
  112. data/app/views/proctolog/bursts/_line.html.erb +28 -0
  113. data/app/views/proctolog/bursts/_merged.html.erb +23 -0
  114. data/app/views/proctolog/bursts/_moved.html.erb +16 -0
  115. data/app/views/proctolog/bursts/_notices.html.erb +39 -0
  116. data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
  117. data/app/views/proctolog/bursts/_rules.html.erb +27 -0
  118. data/app/views/proctolog/bursts/_side.html.erb +49 -0
  119. data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
  120. data/app/views/proctolog/bursts/show.html.erb +54 -0
  121. data/app/views/proctolog/code_reviews/show.html.erb +33 -0
  122. data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
  123. data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
  124. data/app/views/proctolog/decisions/_box.html.erb +27 -0
  125. data/app/views/proctolog/decisions/_choices.html.erb +9 -0
  126. data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
  127. data/app/views/proctolog/decisions/_part.html.erb +61 -0
  128. data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
  129. data/app/views/proctolog/decisions/_theme.html.erb +13 -0
  130. data/app/views/proctolog/decisions/index.html.erb +75 -0
  131. data/app/views/proctolog/notes/_card.html.erb +19 -0
  132. data/app/views/proctolog/notes/_template.html.erb +41 -0
  133. data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
  134. data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
  135. data/app/views/proctolog/prompts/show.html.erb +41 -0
  136. data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
  137. data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
  138. data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
  139. data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
  140. data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
  141. data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
  142. data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
  143. data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
  144. data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
  145. data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
  146. data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
  147. data/app/views/proctolog/pull_requests/show.html.erb +145 -0
  148. data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
  149. data/app/views/proctolog/recordings/_list.html.erb +8 -0
  150. data/app/views/proctolog/recordings/_note.html.erb +22 -0
  151. data/app/views/proctolog/recordings/index.html.erb +33 -0
  152. data/app/views/proctolog/recordings/show.html.erb +118 -0
  153. data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
  154. data/app/views/proctolog/scenarios/_note.html.erb +18 -0
  155. data/app/views/proctolog/scenarios/_open.html.erb +9 -0
  156. data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
  157. data/app/views/proctolog/scenarios/_server.html.erb +20 -0
  158. data/app/views/proctolog/scenarios/index.html.erb +62 -0
  159. data/bin/proctolog +330 -0
  160. data/completions/_proctolog +143 -0
  161. data/config/application.rb +94 -0
  162. data/config/database.yml +16 -0
  163. data/config/environment.rb +6 -0
  164. data/config/initializers/grouse.rb +113 -0
  165. data/config/initializers/grouse_notes.rb +9 -0
  166. data/config/keys.yml +6 -0
  167. data/config/routes.rb +71 -0
  168. data/config/storage.yml +4 -0
  169. data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
  170. data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
  171. data/docs/manual/app.md +157 -0
  172. data/docs/manual/bursts.md +129 -0
  173. data/docs/manual/deciding.md +89 -0
  174. data/docs/manual/desk.png +0 -0
  175. data/docs/manual/films.md +20 -0
  176. data/docs/manual/handover.md +93 -0
  177. data/docs/manual/pr-format.md +75 -0
  178. data/docs/manual/reading.md +233 -0
  179. data/docs/manual/unopened.md +80 -0
  180. data/docs/manual/updates.md +55 -0
  181. data/lib/proctolog/installation.rb +125 -0
  182. data/lib/proctolog/version.rb +7 -0
  183. data/public/asking.js +50 -0
  184. data/public/code-review.js +40 -0
  185. data/public/cursor.js +292 -0
  186. data/public/decision-screenshots.js +97 -0
  187. data/public/decisions.css +949 -0
  188. data/public/decisions.js +1289 -0
  189. data/public/desk.css +582 -0
  190. data/public/desk.js +126 -0
  191. data/public/favicon.png +0 -0
  192. data/public/keys.js +228 -0
  193. data/public/mentions.js +389 -0
  194. data/public/messages.js +103 -0
  195. data/public/notes.js +731 -0
  196. data/public/pick.css +99 -0
  197. data/public/plaque.png +0 -0
  198. data/public/pr-number.js +11 -0
  199. data/public/prompt.css +46 -0
  200. data/public/prompt.js +16 -0
  201. data/public/pull_request.css +805 -0
  202. data/public/recording.css +379 -0
  203. data/public/recording.js +394 -0
  204. data/public/release.js +25 -0
  205. data/public/respond.css +260 -0
  206. data/public/respond.js +95 -0
  207. data/public/review-drafts.js +949 -0
  208. data/public/review-rounds.js +1447 -0
  209. data/public/scenarios.css +253 -0
  210. data/public/scenarios.js +36 -0
  211. data/public/schemas/proctolog-response-v1.json +101 -0
  212. data/public/side.js +133 -0
  213. data/public/standing.css +478 -0
  214. data/public/terminal-apl.svg +6 -0
  215. data/public/terminal-cuneiform.svg +3 -0
  216. data/public/terminal-grain-dots.svg +6 -0
  217. data/public/terminal-grain-weave.svg +6 -0
  218. data/public/terminal-grain-wedges.svg +6 -0
  219. data/public/terminal-shades.svg +3 -0
  220. data/public/terminal.css +4426 -0
  221. data/public/toast.js +26 -0
  222. data/public/try.js +124 -0
  223. data/public/viewer.js +808 -0
  224. data/public/words.js +81 -0
  225. data/skills/proctolog-handover/SKILL.md +77 -0
  226. data/skills/proctolog-pr-writing/SKILL.md +228 -0
  227. data/skills/proctolog-pr-writing/references/example-description.md +33 -0
  228. metadata +366 -0
@@ -0,0 +1,49 @@
1
+ <%# locals: (burst:, at: nil) %>
2
+ <%# The pane at the left of every page of a burst: the plaque the desk is named for, where the desk's pages are, and the route with how far each pull request's decisions are, so the owner always sees where they stand. `at` is the pull request the page is on. Focused on a part, it lists that part, with the key back to the whole burst, and counts what is left in the part against the burst; a pull request opened from outside the part stands apart under it. A page that keeps choices rewrites the counts as they change. data-desk keeps it out of a page's reading order. %>
3
+ <% route = burst.route %>
4
+ <% progress = burst.progress(route) %>
5
+ <aside class="side" aria-label="The burst" data-desk>
6
+ <div class="head">
7
+ <%= link_to image_tag("/plaque.png", alt: "", class: "plaque"), burst_path(burst), tabindex: -1, aria: {hidden: true} %>
8
+ <nav class="desk" aria-label="The desk">
9
+ <%= desk_link "route", burst_path(burst), on: "bursts" %>
10
+ <%= desk_link "decisions", burst_decisions_path(burst), on: "decisions" %>
11
+ <%= desk_link "scenarios", burst_scenarios_path(burst), on: "scenarios" %>
12
+ <%= desk_link "recordings", burst_recordings_path(burst), on: "recordings" %>
13
+ <%= desk_link "prompt", burst_prompt_path(burst), on: "prompts" %>
14
+ <%= desk_link "bursts & PRs", open_pull_requests_path, on: "open_pull_requests" %>
15
+ </nav>
16
+ </div>
17
+ <p class="name"><%= link_to "PRoctolog", burst_path(burst) %> <span class="burst-name"><%= burst.name %></span><% if burst.put_away? %> <span class="away">put away</span><% end %></p>
18
+ <% unless focus.whole? %>
19
+ <p class="focus">Only <%= focus.part.in_words %> · <%= link_to url_for(part: nil), data: {key: "P"} do %><kbd>P</kbd> the whole burst<% end %></p>
20
+ <% end %>
21
+ <%= tag.p class: "overall", data: {side_overall: "", part_prs: (focus.map(&:number).join(" ") unless focus.whole?)} do %>
22
+ <%= side_gauge(progress.each_value.sum(Proctolog::Choices::NONE), part: (focus.sum(Proctolog::Choices::NONE) { progress.fetch(it.number) } unless focus.whole?)) %>
23
+ <% end %>
24
+ <%# Only how many notices there are: they are read at the head of the route. %>
25
+ <% if (notices = burst.notices).any? %>
26
+ <p class="notices"><%= link_to pluralize(notices.count, "notice"), burst_path(burst, anchor: "notices") %></p>
27
+ <% end %>
28
+
29
+ <% focus.parts.each do |part| %>
30
+ <h2><%= part_heading(part, route) %></h2>
31
+ <ol class="stops">
32
+ <% route.stops_of(part).each do |stop| %>
33
+ <%= render "proctolog/bursts/side_stop", burst:, stop:, progress: progress.fetch(stop.number), at: %>
34
+ <% end %>
35
+ </ol>
36
+ <% end %>
37
+ <% if (outside = route.at(at)) && !focus.include?(outside) %>
38
+ <h2>Outside this part</h2>
39
+ <ol class="stops">
40
+ <%= render "proctolog/bursts/side_stop", burst:, stop: outside, progress: progress.fetch(outside.number), at: %>
41
+ </ol>
42
+ <% end %>
43
+
44
+ <%# When GitHub was read: the desk keeps that reading until asked for another. The script asks GitHub, behind the page, whether it has moved on, and says so here. %>
45
+ <%= tag.div class: "read", data: {freshness: burst_freshness_path(burst), read_at: burst.read_at.iso8601} do %>
46
+ <p>GitHub read <%= tag.time read_at_in_words(burst.read_at), datetime: burst.read_at.iso8601 %> · <%= link_to "read again", burst_freshness_path(burst, again: 1, back: request.fullpath), class: "again", data: {key: "r"} %></p>
47
+ <p class="moved" data-moved hidden></p>
48
+ <% end %>
49
+ </aside>
@@ -0,0 +1,10 @@
1
+ <%# locals: (burst:, stop:, progress:, at:) %>
2
+ <%# One pull request of the pane: its mark, its number, its title and how far its decisions are, marked when the page is about it. Held under the pointer, side.js says the same in words, with its title in full and the way to it on GitHub. %>
3
+ <%= tag.li data: {side_pr: stop.number, github: stop.url}, aria: {current: ("page" if stop.number == at)} do %>
4
+ <%= link_to burst_pull_request_path(burst, stop.number) do %>
5
+ <span class="mark" data-side-mark><%= side_mark(progress) %></span>
6
+ <span class="number"><%= stop.number %></span>
7
+ <span class="title"><%= stop.title %></span>
8
+ <span class="fraction" data-side-fraction><%= side_fraction(progress) %></span>
9
+ <% end %>
10
+ <% end %>
@@ -0,0 +1,54 @@
1
+ <% content_for :title, @burst.name %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/standing.css">
4
+ <% end %>
5
+ <% content_for :mode, mode_line("Route", "j k" => "move", "N" => "next to review") %>
6
+ <% content_for :keys do %>
7
+ <%= keys_group "Move", "j k" => "move", "Home End" => "first, last", "Enter" => "open", "N" => "next to review" %>
8
+ <%= keys_group "Focus", "p" => "this part only", "P" => "the whole burst" %>
9
+ <%= keys_group "This burst", "d" => "code in Diffle", "r" => "read GitHub again", "G" => "the PR on GitHub" %>
10
+ <% if [@notices.joined, @notices.left, @notices.opened].any?(&:any?) %>
11
+ <%= keys_group "Notices", "x" => "dismiss the one the cursor is on" %>
12
+ <% end %>
13
+ <% if @rules.any? %>
14
+ <%= keys_group "Rules", "c" => "edit the one the cursor is on", "x" => "withdraw it" %>
15
+ <% end %>
16
+ <% end %>
17
+
18
+ <header class="burst">
19
+ <h1><%= @burst.name %></h1>
20
+ <% shown = @standings.slice(*focus) %>
21
+ <% if @burst.finished? %>
22
+ <p class="tally">Finished: every pull request of it went in, so the desk puts it away.</p>
23
+ <% else %>
24
+ <p class="tally"><%= turns_in_words(shown) %> <%= progress_in_words(shown.each_value.sum(Proctolog::Choices::NONE, &:progress)).upcase_first %>.</p>
25
+ <%= render "put_away", burst: @burst %>
26
+ <% end %>
27
+ <% if @taken_in.any? %>
28
+ <p class="taken-in" role="status">From the agent's reports, <%= pluralize(@taken_in.size, "note") %> moved to addressed: <%= @taken_in.map(&:id).to_sentence %>.</p>
29
+ <% end %>
30
+ <% if (gone = @route.gone - @merged.map(&:number)).any? %>
31
+ <p class="gone">No longer open: <%= gone.map { "##{it}" }.to_sentence %>.</p>
32
+ <% end %>
33
+ </header>
34
+
35
+ <main class="route">
36
+ <% if @notices.any? %>
37
+ <%= render "notices", burst: @burst, notices: @notices %>
38
+ <% end %>
39
+ <% focus.parts.each do |part| %>
40
+ <% stops = @route.stops_of(part) %>
41
+ <h2><%= part_heading(part, @route) %></h2>
42
+ <ol class="lines">
43
+ <% stops.each do |stop| %>
44
+ <%= render "line", burst: @burst, stop:, standing: @standings.fetch(stop) %>
45
+ <% end %>
46
+ </ol>
47
+ <% end %>
48
+ <% if @merged.any? %>
49
+ <%= render "merged", burst: @burst, merged: @merged, shown: @merged_shown %>
50
+ <% end %>
51
+ <% if @rules.any? %>
52
+ <%= render "rules", burst: @burst, rules: @rules %>
53
+ <% end %>
54
+ </main>
@@ -0,0 +1,33 @@
1
+ <% content_for :title, "Code review · ##{@stop.number}" %>
2
+ <% content_for :head do %><link rel="stylesheet" href="/respond.css"><link rel="stylesheet" href="/decisions.css"><% end %>
3
+ <header class="burst">
4
+ <h1>Code review · #<%= @stop.number %></h1>
5
+ <p><%= @stop.title %> · <%= link_to "Back to the PR", burst_pull_request_path(@burst, @stop.number) %></p>
6
+ <div class="actions">
7
+ <%= button_to "This layer only", burst_pull_request_code_review_path(@burst, @stop.number), class: "primary", form: {target: "_blank", data: {code: ""}} %>
8
+ <%= button_to "Whole PR against main", burst_pull_request_code_review_path(@burst, @stop.number), params: {base: "main"}, class: "quiet", form: {target: "_blank"} %>
9
+ </div>
10
+ <p class="reading">Diffle reviews this PR's committed changes in a private checkout. Its comments join your next handover. Press <kbd>c</kbd> to comment, <kbd>v</kbd> to mark a file viewed, and <kbd>?</kbd> for its keys. Reload this page after commenting.</p>
11
+ </header>
12
+ <% if @snapshot %>
13
+ <p>Code opened at <%= @snapshot["openedAt"] %>, head <code><%= @snapshot["head"][0, 8] %></code>.
14
+ <%= {"main" => "Whole PR against main (includes earlier stack layers).", "changes" => "Changes since your reviewed head.", "layer" => "This PR's layer only."}[@snapshot["comparison"]] %>
15
+ <% if @snapshot["prHead"] && @snapshot["head"] != @snapshot["prHead"] %>This is a returned local head; GitHub's head is <code><%= @stop.head_oid[0, 8] %></code>.
16
+ <% elsif @snapshot["head"] != @stop.head_oid %><strong>The PR has changed: reopen Diffle to review its new head.</strong><% end %>
17
+ </p>
18
+ <p><%= @code.files_viewed %> of <%= @snapshot["files"].size %> files marked viewed on that code snapshot.</p>
19
+ <%= button_to "Stop this Diffle server", burst_pull_request_code_review_path(@burst, @stop.number), method: :delete, class: "quiet" %>
20
+ <% end %>
21
+ <h2>Comments</h2>
22
+ <% if @threads.empty? %><p>No code comments yet. Open Diffle and press <kbd>c</kbd> on a line or <kbd>C</kbd> on a file.</p><% end %>
23
+ <ol class="cards">
24
+ <% @threads.each do |thread| %>
25
+ <% anchor = thread.fetch("anchor") %>
26
+ <li class="card">
27
+ <p><code><%= anchor["path"] %><%= ":#{anchor["startLine"]}-#{anchor["endLine"]}" if anchor["kind"] == "line" %></code> · <%= thread["resolved"] ? "Resolved in Diffle" : "Open" %><%= " · Lost its place" if thread["stale"] %></p>
28
+ <% if anchor["quoted"] %><pre class="text"><%= anchor["quoted"] %></pre><% end %>
29
+ <% thread["messages"].each do |message| %><p class="text"><%= message["body"] %></p><% end %>
30
+ </li>
31
+ <% end %>
32
+ </ol>
33
+ <p><%= link_to "Review responses and hand over", burst_prompt_path(@burst) %></p>
@@ -0,0 +1,5 @@
1
+ <%# locals: (box:) %>
2
+ <%# The alternative is said in full beside its key, so choosing it needs no second look at the text. %>
3
+ <% if (alternative = box.decision.alternative).present? && !alternative.match?(/\Anone[.!]?\z/i) %>
4
+ <%= button_tag type: "button", class: "choice", data: {choice_alternative: "", alternative:} do %><kbd>b</kbd> <span class="does">Use the alternative</span> <span class="what"><%= alternative %></span><% end %>
5
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%# locals: (box:, burst:, back:, show: nil) %>
2
+ <%# What stands under a box, on the decisions page and on the pull request's own page: where the box stands, the owner's notes on it, newest last, and what the owner can do with it. A block given to the partial adds a link to the actions. The script writes a note into the slot, to the address and on the words given here; a note saved, a star or a move of a note comes back to `back`. data-desk keeps all of it out of the words of a description, which Rendering and the script measure without it. %>
3
+ <%= tag.div class: "beneath", data: {decision: box.to_param, grade: box.decision.grade, held: held_by(box).join(","), desk: "", notes: burst_pull_request_notes_path(burst, box.stop.number), exact: box.quote.exact, before: box.quote.before, after: box.quote.after, section: Proctolog::Description::DECISIONS, back:} do %>
4
+ <% if (status = status_of(box)).present? %>
5
+ <p class="status"><%= status %></p>
6
+ <% end %>
7
+ <% if box.notes.any? %>
8
+ <ol class="cards">
9
+ <% box.notes.each do |note| %>
10
+ <%= render "proctolog/notes/card", note:, burst:, stop: box.stop, back: %>
11
+ <% end %>
12
+ </ol>
13
+ <% end %>
14
+ <p class="draft-status" data-choice-status></p>
15
+ <div class="actions">
16
+ <%= render "proctolog/decisions/choices", box: %>
17
+ <%= yield if block_given? %>
18
+ </div>
19
+ <div class="choice-slot"></div>
20
+ <div class="slot"></div>
21
+ <% end %>
@@ -0,0 +1,27 @@
1
+ <%# locals: (box:, burst:, filter:, of: nil) %>
2
+ <%# `of` names the pull request above the decision, where the list is not by pull request. %>
3
+ <%= tag.li id: "box-#{box.to_param}", class: "box", tabindex: -1,
4
+ data: {box: "", decision: box.to_param, state: box.state, waiting: box.waiting?, pr: box.stop.number, grade: box.decision.grade, held: held_by(box).join(",")} do %>
5
+ <% if of %><p class="of"><%= pull_request_link burst, of %><%= " · non-blocking" unless box.decision.blocking? %></p><% end %>
6
+ <div class="body"><%= raw decision_html(box, burst) %></div>
7
+ <div class="beneath">
8
+ <% if (status = status_of(box)).present? %><p class="status"><%= status %></p><% end %>
9
+ <% if box.notes.any? %>
10
+ <ol class="cards">
11
+ <% box.notes.each do |note| %>
12
+ <li class="card" data-kind="<%= note.kind %>" data-state="<%= note.state %>">
13
+ <p><strong><%= note.kind.capitalize %></strong>: <%= with_mentions(note.text, burst:) %></p>
14
+ <p class="status"><%= note.state %></p>
15
+ <% if note.reply %><p class="reply"><%= with_mentions(note.reply["text"], burst:) %> <code><%= note.reply["commit"] %></code></p><% end %>
16
+ </li>
17
+ <% end %>
18
+ </ol>
19
+ <% end %>
20
+ <p class="draft-status" data-choice-status></p>
21
+ <div class="actions" aria-label="Review choice">
22
+ <%= render "proctolog/decisions/choices", box: %>
23
+ <%= link_to "On the pull request page", box_on_its_page(burst, box), data: {on_its_page: ""} %>
24
+ </div>
25
+ <div class="choice-slot"></div>
26
+ </div>
27
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%# locals: (box:) %>
2
+ <%# What the owner can choose on a decision, as a menu: the key, then what it does, the same on both pages that show one. %>
3
+ <button type="button" class="choice" data-choice="leave"><kbd>a</kbd> <span class="does">Leave as is</span></button>
4
+ <%= render "proctolog/decisions/alternative", box: %>
5
+ <% {question: ["Ask a question", "q"], change: ["Suggest a change", "c"], later: ["Leave for later", "l"]}.each do |kind, (label, key)| %>
6
+ <button type="button" class="choice" data-choice="<%= kind %>"><kbd><%= key %></kbd> <span class="does"><%= label %></span></button>
7
+ <% end %>
8
+ <button type="button" class="choice" data-make-rule hidden><kbd>R</kbd> <span class="does">Make it a rule</span></button>
9
+ <button type="button" class="choice" data-choice-clear hidden><span class="does">Clear this choice</span></button>
@@ -0,0 +1,47 @@
1
+ <%# locals: (burst:, stop: nil, decisions: nil, filter: nil) %>
2
+ <%# One bar for the owner's choices, on every page that shows them: how far they are, the handover, and behind "Data" the backup, the imports and the counts in full. `stop` narrows it to one pull request, whose page it then is; `decisions` adds the ways to look at the list. The script fills the counts. data-desk keeps the bar out of the reading order of a pull request's page. %>
3
+ <%= tag.section class: ["draft-tools", ("compact" if stop)], data: {review_draft: "", desk: "", repository: burst.repository, burst: burst.slug, pr: stop&.number, part: focus.to_param, part_prs: (focus.map(&:number).join(" ") unless focus.whole?), checkout: Proctolog::Project.current.root.to_s, pictures: Proctolog::Project.current.pictures.to_s} do %>
4
+ <script type="application/json" id="burst-choices"><%= raw json_escape(burst.choices.document.to_json) %></script>
5
+ <div class="bar">
6
+ <div class="where">
7
+ <p class="progress-line" data-progress></p>
8
+ <p class="round-line" data-round-line hidden></p>
9
+ </div>
10
+ <div class="actions">
11
+ <button type="button" class="quiet" popovertarget="choices-data">data</button>
12
+ <%# A popover: it opens over the page from the bar, and Escape or a click elsewhere closes it. %>
13
+ <div class="data" id="choices-data" popover>
14
+ <h2>The choices of this burst</h2>
15
+ <p data-draft-summary></p>
16
+ <p data-round-summary></p>
17
+ <p data-draft-message></p>
18
+ <div class="actions">
19
+ <button type="button" class="quiet" data-download-choices>Back up choices</button>
20
+ <label class="import-choices">Restore choices <input type="file" accept=".json,application/json" data-import-choices></label>
21
+ <label class="import-choices">Import agent response <input type="file" accept=".json,application/json" data-import-response></label>
22
+ <button type="button" class="quiet" data-refresh-review>Refresh code comments and responses</button>
23
+ <% unless stop %><%= link_to "Review agent responses", burst_prompt_path(burst, anchor: "responses") %><% end %>
24
+ </div>
25
+ <p class="explain">The desk keeps your choices in <code>choices.json</code> beside its marks, so any browser on this Mac shows them. A backup is a copy to keep elsewhere. Deferred items become backlog-task requests in the handover<%= ", which covers every pull request of this burst" if stop %>.</p>
26
+ </div>
27
+ <%= tag.button type: "button", class: "primary", data: {hand_over: "", inline_handover: ("" if stop)} do %><kbd>e</kbd> hand over<% end %>
28
+ <%# The handover e last kept on the desk, for a new session in the Claude app to read. The script gives it its address. %>
29
+ <a class="session-link" data-new-session hidden>Open a new session with it</a>
30
+ </div>
31
+ </div>
32
+
33
+ <% if decisions %>
34
+ <nav class="filters" aria-label="Show">
35
+ <button type="button" class="quiet" data-pending-view>To review <span class="count" data-count-of="unreviewed"></span></button>
36
+ <button type="button" class="quiet" data-reviewed-view>Reviewed <span class="count" data-count-of="reviewed"></span></button>
37
+ <button type="button" class="quiet" data-all-view>All <span class="count" data-count-of="all"></span></button>
38
+ <%= link_to safe_join(["Waiting for you", tag.span(decisions.count("waiting"), class: "count")], " "), burst_decisions_path(burst, show: "waiting"), data: {filter: "waiting"} %>
39
+ <%# One kind of question across the burst, or each pull request's in turn. %>
40
+ <%= params[:by] == "theme" ? link_to("By pull request", burst_decisions_path(burst, show: params[:show])) : link_to("By theme", burst_decisions_path(burst, by: "theme", show: params[:show])) %>
41
+ <details class="by-state">
42
+ <summary>As GitHub and your notes have them</summary>
43
+ <p><%= filter_links burst, decisions, filter %></p>
44
+ </details>
45
+ </nav>
46
+ <% end %>
47
+ <% end %>
@@ -0,0 +1,61 @@
1
+ <%# locals: (part:, burst:, decisions:, filter:) %>
2
+ <% stop = part.stop %>
3
+ <section class="part" id="pr-<%= stop.number %>" aria-labelledby="pr-<%= stop.number %>-heading" data-part="<%= stop.number %>">
4
+ <header>
5
+ <h2 id="pr-<%= stop.number %>-heading"><%= pull_request_link burst, stop %></h2>
6
+ <p class="facts">
7
+ <span class="progress" data-part-progress></span> ·
8
+ <span data-part-grades><%= grades_of(part.boxes.map(&:decision)) %></span> ·
9
+ <%= position_of stop %> ·
10
+ <%= stop.url ? link_to("GitHub", stop.url, target: "_blank", rel: "noopener") : not_opened(stop) %>
11
+ <button type="button" class="quiet" data-leave-non-blocking data-pr="<%= stop.number %>" hidden><kbd>A</kbd> <span data-leave-said></span></button>
12
+ </p>
13
+ <% if (reason = burst.refusal(stop.number)) %>
14
+ <p class="refused"><%= reason %>. You can review it here; the handover preserves that restriction.</p>
15
+ <% end %>
16
+ </header>
17
+
18
+ <% part.groups(part.blocking).each do |under, boxes| %>
19
+ <% if under %><h3 class="lead-in"><%= with_code under %></h3><% end %>
20
+ <ol class="boxes">
21
+ <% boxes.each do |box| %>
22
+ <%= render "box", box:, burst:, filter: %>
23
+ <% end %>
24
+ </ol>
25
+ <% end %>
26
+
27
+ <%# The non-blocking decisions fold away: one gesture, A, leaves those that wait for nothing as they are. %>
28
+ <% if part.non_blocking.any? %>
29
+ <details class="non-blocking" data-non-blocking data-pr="<%= stop.number %>">
30
+ <summary>Non-blocking <span class="count" data-non-blocking-count></span></summary>
31
+ <% part.groups(part.non_blocking).each do |under, boxes| %>
32
+ <% if under %><h3 class="lead-in"><%= with_code under %></h3><% end %>
33
+ <ol class="boxes">
34
+ <% boxes.each do |box| %>
35
+ <%= render "box", box:, burst:, filter: %>
36
+ <% end %>
37
+ </ol>
38
+ <% end %>
39
+ </details>
40
+ <% end %>
41
+
42
+ <% if part.questions.any? || part.rewritten.any? %>
43
+ <ol class="boxes">
44
+ <% part.questions.each do |question| %>
45
+ <li class="box question" data-box data-state="open" data-waiting="true">
46
+ <div class="body"><p><%= with_code question.question %></p></div>
47
+ <p class="status">
48
+ <span class="state">A question, with no box</span> · Named by <%= named_by [question] %>
49
+ · <%= link_to "On the pull request page", burst_pull_request_path(burst, stop.number) %>
50
+ </p>
51
+ </li>
52
+ <% end %>
53
+ <% part.rewritten.each do |lead| %>
54
+ <li class="box rewritten" data-box data-state="rewritten">
55
+ <div class="body"><p><%= lead %></p></div>
56
+ <p class="status"><span class="state">Accepted, and rewritten since</span> · The description no longer holds these words.</p>
57
+ </li>
58
+ <% end %>
59
+ </ol>
60
+ <% end %>
61
+ </section>
@@ -0,0 +1 @@
1
+ <script type="application/json" id="review-round-data"><%= raw json_escape(round_data(burst, pr: local_assigns[:stop]&.number).to_json) %></script>
@@ -0,0 +1,13 @@
1
+ <%# locals: (group:, burst:, filter:) %>
2
+ <%# One theme across the burst: its heading, which the cursor rests on like a decision and from which x selects the whole group, then its decisions in the route's order, each with its pull request. %>
3
+ <section class="theme-group" id="theme-<%= group.theme.name %>" data-theme="<%= group.theme.name %>">
4
+ <header>
5
+ <%= tag.h2 tabindex: -1, data: {theme_heading: group.theme.name} do %><%= group.theme.name.upcase_first %> <span class="count"><%= pluralize(group.boxes.size, "decision") %></span><% end %>
6
+ <p class="facts"><%= group.theme.meaning %></p>
7
+ </header>
8
+ <ol class="boxes">
9
+ <% group.boxes.each do |box| %>
10
+ <%= render "box", box:, burst:, filter:, of: box.stop %>
11
+ <% end %>
12
+ </ol>
13
+ </section>
@@ -0,0 +1,75 @@
1
+ <% content_for :title, "Decisions" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/respond.css">
4
+ <link rel="stylesheet" href="/decisions.css">
5
+ <script type="module" src="/decisions.js"></script>
6
+ <% end %>
7
+ <% content_for :mode, mode_line("Deciding", "j k" => "move", "a b q c l" => "choose", "u" => "undo") %>
8
+ <% content_for :keys do %>
9
+ <%= keys_group "Move", "j k" => "move", "[ ]" => "pull request", "Enter" => "open on its page", "h" => "the route" %>
10
+ <%= keys_group "Decide", "a b q c l" => "choose", "A" => "leave this pull request's non-blocking as is", "R" => "make the answer a rule", "u" => "undo", "⌘ Enter" => "save the text", "Esc" => "close, keeping it" %>
11
+ <%= keys_group "Together", "x" => "select a decision, or a theme's", "Esc" => "clear the selection" %>
12
+ <%= keys_group "This burst", "e" => "handover" %>
13
+ <% end %>
14
+
15
+ <% parts = @decisions.parts("all") %>
16
+
17
+ <header class="burst">
18
+ <h1>Decisions</h1>
19
+ <p class="tally">
20
+ Decisions from <%= pluralize(parts.size, "pull request") %>, from the bottom of the stack up.
21
+ “Leave as is” keeps the implementation: it ticks no GitHub box, verifies no claim and approves no merge.
22
+ </p>
23
+ <%# The route answers to h; the pane says when GitHub was read and reads it again. %>
24
+ <nav hidden><%= link_to "Route", burst_path(@burst), data: {key: "h"} %></nav>
25
+ </header>
26
+
27
+ <%= render "proctolog/decisions/draft_tools", burst: @burst, decisions: @decisions, filter: @filter %>
28
+ <script type="application/json" id="decision-inventory"><%= raw json_escape(decision_inventory(@burst)) %></script>
29
+ <%= render "proctolog/decisions/round_data", burst: @burst %>
30
+
31
+ <% if @decisions.unmatched.any? %>
32
+ <section class="unmatched">
33
+ <h2>Named in the burst file, but no box begins so</h2>
34
+ <ul>
35
+ <% @decisions.unmatched.each do |entry| %>
36
+ <li>
37
+ <%= link_to "##{entry.pr}", burst_pull_request_path(@burst, entry.pr), class: "number" %>
38
+ “<%= entry.words %>”, named by <%= entry.by || "the burst file" %>. The description may have been rewritten since; the entry is in <code><%= Proctolog::Project.current.from_root(@burst.file) %></code>.
39
+ </li>
40
+ <% end %>
41
+ </ul>
42
+ </section>
43
+ <% end %>
44
+
45
+ <%= tag.main class: "decisions", data: {initial_filter: @filter, by_theme: @by_theme} do %>
46
+ <% if @by_theme %>
47
+ <% @decisions.by_theme("all").each do |group| %>
48
+ <%= render "theme", group:, burst: @burst, filter: @filter %>
49
+ <% end %>
50
+ <% else %>
51
+ <% parts.each do |part| %>
52
+ <%= render "part", part:, burst: @burst, decisions: @decisions, filter: @filter %>
53
+ <% end %>
54
+ <% end %>
55
+ <% end %>
56
+ <section class="empty queue-empty" data-queue-empty hidden>
57
+ <% if parts.empty? %>
58
+ <p>No decisions in this burst. The desk reads them from “Decisions for the owner” in each pull request's description.</p>
59
+ <%= link_to "Back to the route", burst_path(@burst) %>
60
+ <% else %>
61
+ <p role="status" data-empty-said>No decisions match this view.</p>
62
+ <button type="button" class="quiet" data-all-view>Show all decisions</button>
63
+ <%= link_to "Back to the route", burst_path(@burst) %>
64
+ <% end %>
65
+ </section>
66
+
67
+ <details class="recorded">
68
+ <summary>Recorded elsewhere <span class="count"><%= @records.size %></span></summary>
69
+ <p>The decision records of the repository, under <code>docs/adr/</code>.</p>
70
+ <ul>
71
+ <% @records.each do |record| %>
72
+ <li><code><%= record.file %></code> <%= record.title %></li>
73
+ <% end %>
74
+ </ul>
75
+ </details>
@@ -0,0 +1,19 @@
1
+ <%# locals: (note:, burst:, stop:, where: nil, back: nil, attributes: {}) %>
2
+ <%# One note as a card: what it is, what was said, where it is pinned (`where`), the agent's reply, and the ways to move it along. A press of those comes back to `back` when the page that showed the card gave one. %>
3
+ <%= tag.li(**{class: "card", data: {kind: note.kind, state: note.state}}.deep_merge(attributes)) do %>
4
+ <p class="meta"><span class="kind"><%= note.kind %></span> <span class="state"><%= note.state %></span></p>
5
+ <p class="text"><%= with_mentions(note.text, burst:) %></p>
6
+ <%= where %>
7
+ <% if note.reply %>
8
+ <p class="reply">
9
+ Addressed<%= " in " if note.reply["commit"] %><code><%= note.reply["commit"] %></code>.
10
+ <%= with_mentions(note.reply["text"], burst:) %>
11
+ </p>
12
+ <% end %>
13
+ <div class="moves">
14
+ <% path = burst_pull_request_note_path(burst, stop.number, note.id) %>
15
+ <%= button_to "Confirm", path, method: :patch, params: {state: "confirmed", return: back}.compact, class: "quiet" if note.addressed? %>
16
+ <%= button_to "Reopen", path, method: :patch, params: {state: "open", return: back}.compact, class: "quiet" unless note.open? %>
17
+ <%= button_to "Withdraw", path, method: :patch, params: {state: "withdrawn", return: back}.compact, class: "quiet" unless note.withdrawn? %>
18
+ </div>
19
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <%# locals: (action:) %>
2
+ <%# The form of a note, once for the page. The script copies it into the slot under whatever the note is about, sets where it goes and what it is on, and fills the fields of the place; the fieldsets of a place stay disabled until then. A note written before is opened in it again with its words, sent to the note itself, and may be withdrawn there. The token is the page's own, as the address is not known until a note is chosen. %>
3
+ <%# Without the script nothing here can open, so the buttons that would are not shown. %>
4
+ <% content_for :head do %>
5
+ <noscript><style>[data-respond] { display: none; }</style></noscript>
6
+ <% end %>
7
+ <template id="note-form">
8
+ <div class="respond" data-desk>
9
+ <%= form_with url: action, authenticity_token: form_authenticity_token do |form| %>
10
+ <p class="on" data-on hidden></p>
11
+
12
+ <fieldset class="kinds">
13
+ <legend class="visually-hidden">What kind of note</legend>
14
+ <% Proctolog::Note::KINDS.each do |kind| %>
15
+ <label><%= form.radio_button :kind, kind, id: nil %> <%= kind.capitalize %></label>
16
+ <% end %>
17
+ </fieldset>
18
+
19
+ <%= form.text_area :text, id: nil, rows: 3, required: true, "aria-label": "The note", placeholder: "What should change, or what do you ask?" %>
20
+
21
+ <fieldset name="quote" disabled hidden>
22
+ <% %w[exact before after section].each do |part| %>
23
+ <%= hidden_field_tag "quote[#{part}]", nil, id: nil %>
24
+ <% end %>
25
+ </fieldset>
26
+ <fieldset name="picture" disabled hidden>
27
+ <% %w[picture asset box].each do |part| %>
28
+ <%= hidden_field_tag "picture[#{part}]", nil, id: nil %>
29
+ <% end %>
30
+ </fieldset>
31
+ <%= hidden_field_tag :return, nil, id: nil, disabled: true %>
32
+ <%= hidden_field_tag :_method, "patch", id: nil, disabled: true %>
33
+
34
+ <div class="buttons">
35
+ <%= form.button "Save note", class: "primary" %>
36
+ <button type="submit" class="quiet" name="state" value="withdrawn" formnovalidate data-withdraw hidden>Withdraw</button>
37
+ <button type="button" class="quiet" data-cancel>Cancel</button>
38
+ </div>
39
+ <% end %>
40
+ </div>
41
+ </template>
@@ -0,0 +1,30 @@
1
+ <%# locals: (bursts:, read_at:) %>
2
+ <%# The pane at the left where no burst is open: the plaque, where the desk's pages are, and the bursts to open, the newest first, each with how far its decisions are. Those put away stay off it, as on the page. %>
3
+ <aside class="side" aria-label="The desk" data-desk>
4
+ <div class="head">
5
+ <%= link_to image_tag("/plaque.png", alt: "", class: "plaque"), open_pull_requests_path, tabindex: -1, aria: {hidden: true} %>
6
+ <nav class="desk" aria-label="The desk">
7
+ <%= desk_link "bursts & PRs", open_pull_requests_path, on: "open_pull_requests" %>
8
+ </nav>
9
+ </div>
10
+ <p class="name"><%= link_to "PRoctolog", open_pull_requests_path %> <span class="burst-name">no burst open</span></p>
11
+ <% if bursts.any? %>
12
+ <h2>Your bursts</h2>
13
+ <ol class="stops">
14
+ <% bursts.each do |burst| %>
15
+ <% progress = burst.progress(burst.route).each_value.sum(Proctolog::Choices::NONE) %>
16
+ <li>
17
+ <%= link_to burst_path(burst) do %>
18
+ <span class="mark"><%= side_mark(progress) %></span>
19
+ <span class="number"></span>
20
+ <span class="title"><%= burst.name %></span>
21
+ <span class="fraction"><%= side_fraction(progress) %></span>
22
+ <% end %>
23
+ </li>
24
+ <% end %>
25
+ </ol>
26
+ <% end %>
27
+ <div class="read">
28
+ <p>GitHub read <%= tag.time read_at_in_words(read_at), datetime: read_at.iso8601 %> · <%= link_to "read again", open_pull_requests_path(fresh: 1), class: "again" %></p>
29
+ </div>
30
+ </aside>