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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 784717d224b472eba9ce15227d5ffb578dabce10741c802d6cd62e642f8aee96
4
+ data.tar.gz: 879591d6f7db0c4448563096e0e82ed566f68364c2c55e5b0ac03a739855a207
5
+ SHA512:
6
+ metadata.gz: 0e7c19af833d9fe3099f47e221f7663cd9c43066dcdf08b7a96b48e04374af2ac4f4017158dc308f244a07abc827d78ba0563ae85e9d2f9a3c9bdcfb5bc175cc
7
+ data.tar.gz: 5da7be3fd3fcd54977e57f350b6f496ddda3d70b5c10982976116d8e1d814fc052a503526039195065f73ce1587415fb938fa251b93e8156313c4bb0b335f1dc
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ What changed in each version of the `proctolog` gem, newest first. Versions
4
+ are semantic ([docs/manual/updates.md](docs/manual/updates.md)); a release
5
+ that asks something of you says what under **You do**.
6
+
7
+ ## 0.1.0
8
+
9
+ The first one published as a gem.
10
+
11
+ - `rvx proctolog install`, or
12
+ `gem install proctolog` and `proctolog install`, in place of cloning the
13
+ repository. Under rv it runs on a Ruby 4 of its own, whatever Ruby the shell
14
+ has, and the apps it starts never inherit it.
15
+ - `proctolog version`, and `proctolog update`, which takes a newer version
16
+ however yours was installed: rv, RubyGems, or a checkout it pulls forward.
17
+ - The desk asks RubyGems once a day for the latest version, and the next page
18
+ says when it's newer than yours.
19
+
20
+ **You do**, coming from a checkout: nothing, if you keep it: `proctolog
21
+ update` pulls it from now on. To move to the gem, remove
22
+ `~/.local/bin/proctolog`, the checkout's launcher, then install it one of the
23
+ ways above.
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # PRoctolog
2
+
3
+ A review desk for stacks of pull requests an agent wrote. It runs on your Mac,
4
+ reads the PRs from GitHub, and walks you through every decision their
5
+ descriptions ask of you. What you answered becomes a handover: a short brief and
6
+ a file for each PR, for the agent to work through.
7
+
8
+ ![Deciding the first of a PR's decisions, with the pictures it links](docs/manual/desk.png)
9
+
10
+ It's rough. It has also been through a stack of 21 PRs and 439 decisions, so
11
+ it works.
12
+
13
+ ## The short version
14
+
15
+ Install it with [rv](https://rv.dev), then run it from a checkout of the repo
16
+ whose PRs you review:
17
+
18
+ ```sh
19
+ rvx proctolog install
20
+ cd path/to/your-repo
21
+ proctolog doctor # fix what it marks !!
22
+ proctolog serve # then open http://localhost:4040
23
+ ```
24
+
25
+ Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
26
+ your agent opens PRs, tell it to "write these PRs for PRoctolog", so they come
27
+ with decisions to review. Everything below is the long version.
28
+
29
+ ## Install
30
+
31
+ You need `gh` signed in with access to the repository you review, and either
32
+ [rv](https://rv.dev) or Ruby 4.0. Chrome, [Diffle](https://github.com/moritzwilksch/diffle),
33
+ ImageMagick and ffmpeg each switch something on, and nothing here installs
34
+ them; `doctor` says what each adds, and Homebrew has them all:
35
+
36
+ ```sh
37
+ brew install --cask google-chrome
38
+ brew install diffle imagemagick ffmpeg
39
+ ```
40
+
41
+ With rv, whatever Ruby your shell or your repo runs:
42
+
43
+ ```sh
44
+ rvx proctolog install
45
+ proctolog doctor
46
+ ```
47
+
48
+ rv gives PRoctolog a Ruby 4 and gems of its own, and `install` puts a
49
+ `proctolog` in `~/.local/bin` that runs it through `rvx`. The apps you review
50
+ keep their own Ruby: what the desk starts for them never sees PRoctolog's.
51
+ rv installs from gem.coop, which mirrors RubyGems.
52
+
53
+ With Ruby 4.0 already in your shell:
54
+
55
+ ```sh
56
+ gem install proctolog
57
+ proctolog install
58
+ proctolog doctor
59
+ ```
60
+
61
+ Either way `install` also links the zsh completion into `~/.zsh/completions`
62
+ and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`. `doctor`
63
+ says what's missing, a line each, with the fix: `!!` for what the desk can't do
64
+ without, `--` for what only turns a feature off.
65
+
66
+ When a newer PRoctolog is out, the desk says so, and `proctolog update` takes
67
+ it ([versions and updates](docs/manual/updates.md)).
68
+
69
+ Working on PRoctolog itself? Run it from a checkout instead: clone it, then
70
+ `bundle install && ruby bin/install`, which also puts a `proctolog` that runs
71
+ the checkout in `~/.local/bin`. Keep the checkout where it is, since
72
+ everything links back to it. The desk tells a checkout of a newer release
73
+ too, and `proctolog update` pulls it forward.
74
+
75
+ ## Your first review
76
+
77
+ ```sh
78
+ cd path/to/your-repo
79
+ proctolog serve
80
+ ```
81
+
82
+ Open <http://localhost:4040>. It lists the repo's open PRs: GitHub's stacks
83
+ whole, then the rest, yours first. Press "Review alone" on one, "Review the
84
+ stack" on a stack, or tick a few and review them together. That's all the setup
85
+ there is.
86
+
87
+ On a PR, press `?` for the keys, `f` for the first decision waiting on you, and
88
+ `e` for the handover. Give your agent its brief, or open the new session the
89
+ page offers, which already has. Or skip the page: tell your agent to "check my
90
+ PRoctolog comments", halfway through if you like, and it runs
91
+ `proctolog handover` and takes what you decided so far. Either way it answers
92
+ with a JSON file PRoctolog reads back, so you can confirm each change or push
93
+ back.
94
+
95
+ What you review together is a burst. The page writes one for each pick, kept
96
+ under `~/.local/share/proctolog/` with your marks, never in the repo; "open
97
+ PRs" at the top of any page goes back to the list. From the terminal it's
98
+ `proctolog burst new login-fixes --top 1120 --pr 1131`, where `--top` brings
99
+ the chain of PRs under it, `--pr` one alone and `--stack` a stack of GitHub's.
100
+
101
+ Your review is saved as you go, on your Mac under `~/.local/share/proctolog/`.
102
+
103
+ ## PRs PRoctolog reads well
104
+
105
+ PRoctolog finds decisions under a `## Decisions for the owner` heading, one
106
+ checkbox line each. The `proctolog-pr-writing` skill teaches your agent the
107
+ format, and `proctolog install` already put it where the agent looks. Tell it to
108
+ "write these PRs for PRoctolog", or "rewrite this PR's description for
109
+ PRoctolog" for one that's open. Check a description before publishing it:
110
+
111
+ ```sh
112
+ proctolog check --file body.md
113
+ proctolog check 1131
114
+ ```
115
+
116
+ Not ready to open PRs? Tell your agent to write them for PRoctolog without
117
+ opening them. The desk reviews each from a folder on your Mac as `#01`, `#02`,
118
+ and when the branches are opened, the review moves to their real numbers
119
+ ([how](docs/manual/unopened.md)).
120
+
121
+ ## Scenarios
122
+
123
+ A scenario button starts the app, signs the right member in and opens Chrome
124
+ on the right page. The desk doesn't know your app; it runs the few commands
125
+ the repo's `.proctolog/app.yml` names ([how](docs/manual/app.md)), on your
126
+ own checkout and a scratch database the first Start makes. An app with
127
+ Grouse in it can open Grouse's overlay there too, labelled with the scenario,
128
+ and what you say or type in it comes back as notes on that scenario
129
+ ([how](docs/manual/app.md#grouses-overlay)). Each bold heading
130
+ with numbered steps in a PR's "How to test locally" is a scenario already. A
131
+ `scenarios/<burst>.yml` beside the burst only adds a starting state and page
132
+ ([how](docs/manual/bursts.md)). A repo without an `app.yml` still lists each
133
+ scenario's steps, and you mark what you saw.
134
+
135
+ Films and before/after pairs need a camera the desk doesn't have yet; it's
136
+ next, on the same `app.yml`.
137
+
138
+ ## Notes on the desk itself
139
+
140
+ Grouse is private, so the desk runs without it, and the gem never carries it.
141
+ In a checkout of PRoctolog, once you can read
142
+ [its repo](https://github.com/LemuelCushing/grouse-rails), turn it on:
143
+
144
+ ```sh
145
+ bundle config set --local with grouse && bundle install
146
+ ```
147
+
148
+ Then every page of the desk carries Grouse's overlay, at the bottom right: say or type what's wrong with the page you're
149
+ on, and the note keeps the page, the templates that drew it, what you pointed
150
+ at and, if you ask, a screenshot. Read them at <http://localhost:4040/grouse>.
151
+ An agent working on the desk reads them through Grouse's export API;
152
+ [AGENTS.md](AGENTS.md) tells it how. They're kept under
153
+ `~/.local/share/proctolog/grouse_notes`, one store for every repo you review.
154
+
155
+ A voice note is written out by ElevenLabs, and every note written up as an
156
+ issue by Anthropic, with keys the desk reads from 1Password when it serves:
157
+ `config/keys.yml` holds the references, never the keys, and `op` (1Password's
158
+ CLI, signed in) reads them. Point them at your own items there if yours live
159
+ elsewhere.
160
+
161
+ Without a key a voice note arrives "audio only", with a recording that plays,
162
+ and a note's page says "No structurer set up". `bin/rails grouse:doctor`, in the
163
+ desk's checkout, says what Grouse is missing.
164
+
165
+ ## More
166
+
167
+ - [Bursts, the route and where things live](docs/manual/bursts.md)
168
+ - [How the desk runs your app](docs/manual/app.md)
169
+ - [Reading a PR by keyboard](docs/manual/reading.md)
170
+ - [Deciding](docs/manual/deciding.md)
171
+ - [Handover and the next pass](docs/manual/handover.md)
172
+ - [Writing a PR the desk reads](docs/manual/pr-format.md)
173
+ - [Before anything is opened](docs/manual/unopened.md)
174
+ - [Films](docs/manual/films.md)
175
+ - [Versions and updates](docs/manual/updates.md)
176
+ - [CONCEPTS.md](CONCEPTS.md) for the words, [ROADMAP.md](ROADMAP.md) for what's next
177
+
178
+ Working on the desk itself? Start at [AGENTS.md](AGENTS.md).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # Ticking boxes of one description, on GitHub or in the file of one nobody
5
+ # has opened yet. The first page shows which boxes and the command;
6
+ # nothing is ticked until the second press.
7
+ class AcceptancesController < ApplicationController
8
+ include WithDecisions
9
+
10
+ rescue_from(Acceptance::Refused, Github::Unreachable) { redirect_to decisions_path_for(number), alert: it.message }
11
+
12
+ def new
13
+ @boxes = decisions.to_accept(number, positions)
14
+ @stop = @boxes.first.stop
15
+ @acceptance = @burst.acceptance(number, @boxes.map(&:decision))
16
+ end
17
+
18
+ def create
19
+ boxes = decisions.to_accept(number, positions)
20
+ leads = @burst.acceptance(number, boxes.map(&:decision)).carry_out(seen: params.expect(:digest))
21
+ @burst.marks(number).accept(leads)
22
+ redirect_to decisions_path_for(number), notice: "Ticked #{leads.size} #{"box".pluralize(leads.size)} of ##{number} #{PullRequest.unopened?(number) ? "in its file" : "on GitHub"}."
23
+ end
24
+
25
+ private
26
+
27
+ def positions = params.expect(positions: []).map(&:to_i)
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ class ApplicationController < ActionController::Base
5
+ rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
6
+ rescue_from(ActionController::InvalidAuthenticityToken) do
7
+ redirect_back_or_to root_path, alert: "This page was older than the desk's last start, so nothing was saved. Do it once more."
8
+ end
9
+ rescue_from(Burst::Missing) { render plain: it.message, status: :not_found }
10
+ # A button's press comes back to the page it was pressed on, with the
11
+ # sentence. A page that cannot be shown says it in place: going back to
12
+ # it would only ask again.
13
+ rescue_from(Failure) do
14
+ request.get? ? render(plain: it.message, status: :internal_server_error) : redirect_back_or_to(root_path, alert: it.message.truncate(1200))
15
+ end
16
+
17
+ helper_method :focus
18
+
19
+ private
20
+
21
+ # The part of the burst the owner is taking on its own, from `part`. One
22
+ # the burst does not have leaves the whole burst in sight, and says so.
23
+ def focus
24
+ @focus ||= Focus.on(@burst.route, params[:part]).tap do |focus|
25
+ flash.now[:alert] = "This burst has no part #{params[:part]}, so the whole burst is shown." if params[:part].present? && focus.whole?
26
+ end
27
+ end
28
+
29
+ # Every address a page of a focused burst makes keeps the focus.
30
+ def default_url_options = (@burst && params[:part].present?) ? {part: focus.to_param}.compact : {}
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ class BurstsController < ApplicationController
5
+ # The burst written last, or the open pull requests to pick one from.
6
+ def index = redirect_to(Burst.latest&.then { burst_path(it) } || open_pull_requests_path)
7
+
8
+ # The route of a burst. `fresh` reads GitHub again, then comes back to an
9
+ # address that a reload can repeat without asking GitHub each time.
10
+ def show
11
+ @burst = Burst.find(params.expect(:id))
12
+ @route = @burst.route(fresh: params.key?(:fresh))
13
+ return redirect_to(burst_path(@burst)) if params.key?(:fresh)
14
+
15
+ @taken_in = @burst.reports.take_in
16
+ @standings = @burst.standings(@route)
17
+ @notices = @burst.notices
18
+ @rules = @burst.rules.select { it.within?(focus) }
19
+ @merged = @burst.merged
20
+ @merged_shown = params.key?(:merged) || @burst.finished?
21
+ end
22
+
23
+ # A burst of the pull requests ticked on the open pull requests, each the
24
+ # top of whatever chain is under it, named as the owner typed or else
25
+ # after the first one's branch.
26
+ def create
27
+ tops = params.fetch(:tops, []).map(&:to_i)
28
+ tops.any? or raise Failure, "Tick the pull requests to review together first."
29
+ picked = Github.new(Project.current.github_repository).open_pull_requests.select { tops.include?(it.number) }
30
+ slug = params[:slug].presence || Pick.slug_for(picked)
31
+ redirect_to burst_path(Burst.from(Burst.start(slug, name: params[:name].presence, tops:)).slug)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The choices of a burst as the pages' script keeps them: read whole, and
5
+ # written whole when the writing is newer than the one kept.
6
+ class ChoicesController < ApplicationController
7
+ before_action { @burst = Burst.find(params.expect(:burst_id)) }
8
+
9
+ rescue_from(Choices::Stale) { render json: {error: it.message, document: choices.document}, status: :conflict }
10
+ rescue_from(JSON::ParserError, KeyError) { render json: {error: "These are not the choices of #{@burst.name}."}, status: :unprocessable_content }
11
+
12
+ def show = render(json: {document: choices.document})
13
+
14
+ # A desk from before bursts had their name reads `review`, so both are
15
+ # written: one rolled back can still read what this one saved.
16
+ def update
17
+ incoming = JSON.parse(request.raw_post)
18
+ mine?(incoming) or raise KeyError
19
+ choices.keep(incoming.merge("burst" => @burst.slug, "review" => @burst.slug))
20
+ render json: {revision: choices.revision}
21
+ end
22
+
23
+ private
24
+
25
+ def choices = @choices ||= Choices.of(@burst)
26
+
27
+ # A page from before bursts had their name says whose they are as `review`.
28
+ def mine?(incoming) = incoming.is_a?(Hash) && incoming["repository"] == @burst.repository && incoming.fetch("burst") { incoming["review"] } == @burst.slug
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ class CodeReviewsController < ApplicationController
5
+ include PullRequestScoped
6
+
7
+ # The code comments of a pull request; as JSON, the tally its page's
8
+ # outline asks for again whenever the owner comes back to that tab.
9
+ def show
10
+ @code = CodeReview.new(@burst, @stop.number)
11
+ respond_to do |format|
12
+ format.html { @snapshot, @threads = @code.snapshot, @code.threads }
13
+ format.json { render json: @code.tally.to_h }
14
+ end
15
+ end
16
+
17
+ def create
18
+ redirect_to CodeReview.new(@burst, @stop.number).start(@stop, head: params[:head], base: params[:base]), allow_other_host: true
19
+ end
20
+
21
+ def destroy
22
+ CodeReview.new(@burst, @stop.number).stop_server
23
+ redirect_to burst_pull_request_code_review_path(@burst, @stop.number)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # Every decision of the burst on one page, and the star that marks one as
5
+ # needing thought, which goes back to the page it was pressed on.
6
+ class DecisionsController < ApplicationController
7
+ include WithDecisions
8
+
9
+ # `fresh` reads GitHub again, then comes back to an address a reload can repeat.
10
+ def index
11
+ if params.key?(:fresh)
12
+ @burst.route(fresh: true)
13
+ return redirect_to(burst_decisions_path(@burst, show: params[:show]))
14
+ end
15
+
16
+ @burst.reports.take_in
17
+ # Listed and counted within the focus; the stars and ticks it sends are
18
+ # found among the whole burst's.
19
+ @decisions = @burst.decisions(pages:, focus:)
20
+ @filter = @decisions.filter_for(params[:show] || "open")
21
+ # By pull request, or by theme: one kind of question across the burst.
22
+ @by_theme = params[:by] == "theme"
23
+ @kept = params[:kept]
24
+ @records = DecisionRecord.all
25
+ end
26
+
27
+ def update
28
+ box = decisions.box(number, params.expect(:position).to_i, params.expect(:lead))
29
+ @burst.marks(number).star(box.lead, on: params.expect(:starred) == "1")
30
+ redirect_to url_from(params[:return].to_s) || burst_decisions_path(@burst, anchor: "box-#{box.to_param}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The address a scenario's button hands a browser, good once: it sets the
5
+ # cookies that sign the browser in on the app and sends it there.
6
+ class EntriesController < ApplicationController
7
+ def show
8
+ visit = Visit.take(params.expect(:token)) or return render(plain: "This address was good once and has been used. Press the scenario's button again.", status: :gone)
9
+
10
+ response.headers["Set-Cookie"] = visit.cookies if visit.cookies.any?
11
+ redirect_to visit.to, allow_other_host: true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The pictures an agent's answer rests on, shown where the answer is read.
5
+ class EvidenceController < ApplicationController
6
+ before_action { @evidence = Evidence.new(Burst.find(params.expect(:burst_id))) }
7
+
8
+ # Retaken under the same name as often as the agent works again, so never kept by the browser.
9
+ def show
10
+ file = @evidence.picture(params.expect(:path)) or return head(:not_found)
11
+
12
+ send_file file, type: Rack::Mime.mime_type(file.extname.downcase), disposition: :inline
13
+ end
14
+
15
+ # Where a before and its after differ, for the viewer to outline and go through.
16
+ def compare
17
+ files = params.expect(:before, :after).map { @evidence.picture(it) }
18
+ return head(:not_found) unless files.all?
19
+
20
+ render json: Difference.between(*files, cache: Project.current.cache).to_h.slice(:compared, :same, :sizes, :boxes)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # How fresh the desk's reading of GitHub is. A page asks whether GitHub has
5
+ # moved on once it is shown, so that opening a page never waits on GitHub.
6
+ # With `again` the desk reads GitHub anew and goes back to the page that
7
+ # asked, as the pages' own "Read again" links do.
8
+ class FreshnessController < ApplicationController
9
+ before_action { @burst = Burst.find(params.expect(:burst_id)) }
10
+
11
+ def show
12
+ return again if params.key?(:again)
13
+
14
+ render json: {read_at: @burst.read_at.iso8601, **@burst.moved}
15
+ rescue Github::Unreachable
16
+ # A GitHub that does not answer says nothing has moved: the question is a courtesy.
17
+ render json: {changed: [], closed: []}
18
+ end
19
+
20
+ private
21
+
22
+ def again
23
+ @burst.route(fresh: true)
24
+ redirect_to url_from(params[:back].to_s) || burst_path(@burst)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The owner's "good to go" for the commit and the description a pull
5
+ # request has now, or the taking of it back.
6
+ class GoodToGoController < ApplicationController
7
+ include PullRequestScoped
8
+
9
+ # The mark is for what the owner was looking at: the page sends the commit
10
+ # and the description it showed, and GitHub is read again at this moment.
11
+ # A push or an edit in between means the mark is not given.
12
+ def create
13
+ now = @burst.route(fresh: true).at(@stop.number)
14
+
15
+ if now && [now.head_oid, now.description.digest] == [params[:head], params[:description]]
16
+ marks.settle(head: now.head_oid, described_at: now.described_at, description: now.description.digest)
17
+ back_to_the_page(anchor: "standing")
18
+ else
19
+ redirect_to burst_pull_request_path(@burst, @stop.number),
20
+ alert: "##{@stop.number} moved on GitHub since this page was read, so it was not marked. Look at it again as it is now."
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ marks.unsettle
26
+ back_to_the_page(anchor: "standing")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The owner moving a burst's kept membership on: by dismissing a notice
5
+ # that a pull request joined or left, or by handing the burst over.
6
+ class MembershipsController < ApplicationController
7
+ before_action { @burst = Burst.find(params.expect(:burst_id)) }
8
+
9
+ def update
10
+ @burst.membership.acknowledge(PullRequest.number(params.expect(:number)), @burst.route)
11
+ redirect_to burst_path(@burst, anchor: "notices")
12
+ end
13
+
14
+ # The pages' script says so once a handover is downloaded.
15
+ def handed_over
16
+ @burst.membership.hand_over(@burst.route)
17
+ head :no_content
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The pull requests a # may mean, asked as the owner types one into any box
5
+ # of the desk: mentions.js offers them under it. A checkout with no GitHub
6
+ # repository, or digits that are not digits, mean none.
7
+ class MentionsController < ApplicationController
8
+ def index
9
+ mentions = Mentions.here
10
+ digits = params[:number].to_s[/\A\d{1,9}\z/]
11
+ return render(json: []) unless mentions && digits
12
+
13
+ render json: mentions.matching(digits).map(&:to_h)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # A note pinned to a moment of a recording, and moving it along. Both go
5
+ # back to the player, which is where the owner was.
6
+ class MomentsController < ApplicationController
7
+ include NoteParams
8
+
9
+ before_action { @burst = Burst.find(params.expect(:burst_id)) }
10
+
11
+ def create
12
+ recording = Recording.find(@burst, params.expect(:recording)) or return head(:not_found)
13
+ time = Float(params.expect(:time), exception: false) or raise ActionController::BadRequest, "A moment is a time in seconds"
14
+ moment = recording.moment(time, box: params[:box].presence&.then { box_of(it) }, keep_in: Marks.directory_of(@burst).join("frames"))
15
+ note = @burst.marks(recording.folder).pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place: moment)
16
+ redirect_to burst_recording_path(@burst, recording.to_param, anchor: "note-#{note.id}")
17
+ end
18
+
19
+ # A note's id begins with the folder of its recording: "sitting-3", "1085-2".
20
+ def update
21
+ id = params.expect(:id)
22
+ @burst.marks(id[/\A(sitting|\d+)-\d+\z/, 1].to_s).move(id, to: one_of(Note::STATES, params.expect(:state))) or return head(:not_found)
23
+ redirect_back_or_to burst_recordings_path(@burst)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # What a controller that writes notes asks of its parameters.
5
+ module NoteParams
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def one_of(choices, value) = choices.include?(value) ? value : raise(ActionController::BadRequest, "#{value.inspect} is none of #{choices.to_sentence}")
11
+
12
+ # A picture's id as the pictures route takes it, and nothing else, since
13
+ # it is kept in the note as it was sent.
14
+ def asset_of(id)
15
+ Picture.asset?(id) ? id : raise(Failure, "That is not the id of a picture, so no note was saved.")
16
+ end
17
+
18
+ # "x,y,width,height", each a fraction of the picture.
19
+ def box_of(text)
20
+ box = text.split(",").map { Float(it, exception: false) }
21
+ (box.size == 4 && box.all? { it&.between?(0, 1) }) ? box : raise(ActionController::BadRequest, "A box is four fractions of the picture, not #{text.inspect}")
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # Writing a note, and moving it along: both come back to the page that sent
5
+ # them when it said where (a note under a box is written on the decisions page
6
+ # or on the pull request's, and stays there), else to the pull request's page
7
+ # at the note.
8
+ class NotesController < ApplicationController
9
+ include NoteParams
10
+
11
+ before_action { @burst = Burst.find(params.expect(:burst_id)) }
12
+
13
+ # A note written in the picture viewer is saved without leaving it: the
14
+ # answer says where the page shows it, for once the viewer closes.
15
+ def create
16
+ note = marks.pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place:)
17
+ respond_to do |format|
18
+ format.html { back_to note }
19
+ format.json { render json: {at: at(note), id: note.id}, status: :created }
20
+ end
21
+ end
22
+
23
+ # A state sent moves the note along; words sent without one are the note
24
+ # written again, as the picture viewer opens a note drawn on a picture.
25
+ def update
26
+ note = (params.key?(:state) ? moved : revised) or return head(:not_found)
27
+ respond_to do |format|
28
+ format.html { back_to note }
29
+ format.json { render json: {at: at(note), id: note.id} }
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def marks = @marks ||= @burst.marks(PullRequest.number(params.expect(:pull_request_number)))
36
+
37
+ def moved = marks.move(params.expect(:id), to: one_of(Note::STATES, params.expect(:state)))
38
+
39
+ def revised = marks.revise(params.expect(:id), kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip)
40
+
41
+ def back_to(note) = redirect_to(url_from(params[:return].to_s) || at(note))
42
+
43
+ def at(note) = burst_pull_request_path(@burst, marks.number, anchor: "note-#{note.id}")
44
+
45
+ # Words of the description, a region of a picture, or neither: the whole pull request.
46
+ def place
47
+ if params.key?(:quote)
48
+ quote = params.expect(quote: %i[exact before after section])
49
+ Note::Quote.new(exact: quote.require(:exact), before: quote[:before].presence, after: quote[:after].presence, section: quote[:section].presence)
50
+ elsif params.key?(:picture)
51
+ picture = params.expect(picture: %i[picture asset box])
52
+ Note::Region.new(picture: picture.require(:picture), asset: asset_of(picture.require(:asset)), box: box_of(picture.require(:box)))
53
+ end
54
+ end
55
+ end
56
+ end