plutonium 0.62.2 → 0.63.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 (255) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +44 -0
  3. data/.claude/skills/plutonium-app/SKILL.md +3 -3
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +191 -0
  5. data/.claude/skills/plutonium-auth/SKILL.md +36 -0
  6. data/.claude/skills/plutonium-behavior/SKILL.md +121 -24
  7. data/.claude/skills/plutonium-kanban/SKILL.md +17 -3
  8. data/.claude/skills/plutonium-resource/SKILL.md +259 -12
  9. data/.claude/skills/plutonium-tenancy/SKILL.md +32 -3
  10. data/.claude/skills/plutonium-ui/SKILL.md +115 -14
  11. data/.claude/skills/plutonium-wizard/SKILL.md +73 -4
  12. data/CHANGELOG.md +67 -0
  13. data/CLAUDE.md +87 -0
  14. data/Rakefile +34 -0
  15. data/SECURITY.md +1 -1
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +685 -102
  18. data/app/assets/plutonium.js.map +4 -4
  19. data/app/assets/plutonium.min.js +53 -53
  20. data/app/assets/plutonium.min.js.map +4 -4
  21. data/app/views/rodauth/_login_form.html.erb +13 -0
  22. data/db/migrate/async_interactions/20260817000001_create_plutonium_async_runs.rb +170 -0
  23. data/docs/.vitepress/config.ts +81 -3
  24. data/docs/.vitepress/theme/blog.data.ts +44 -0
  25. data/docs/.vitepress/theme/components/BlogIndex.vue +87 -0
  26. data/docs/.vitepress/theme/components/BlogMeta.vue +47 -0
  27. data/docs/.vitepress/theme/components/HomeFeatureTour.vue +293 -0
  28. data/docs/.vitepress/theme/components/HomeHero.vue +3 -3
  29. data/docs/.vitepress/theme/components/HomeInTheBox.vue +8 -0
  30. data/docs/.vitepress/theme/components/HomeStopWriting.vue +1 -0
  31. data/docs/.vitepress/theme/components/HomeWhyPlutonium.vue +84 -0
  32. data/docs/.vitepress/theme/index.ts +8 -4
  33. data/docs/blog/association-inputs-post-signed-ids.md +70 -0
  34. data/docs/blog/fix-the-model-not-the-policy.md +122 -0
  35. data/docs/blog/fractional-ordering-runs-out-of-room.md +67 -0
  36. data/docs/blog/half-finished-forms-are-pii.md +76 -0
  37. data/docs/blog/index.md +13 -0
  38. data/docs/blog/interactions-are-presentation-objects.md +152 -0
  39. data/docs/blog/introducing-plutonium.md +253 -0
  40. data/docs/blog/jobs-are-not-permission-snapshots.md +100 -0
  41. data/docs/blog/plutonium-and-ai-agents.md +48 -0
  42. data/docs/blog/realtime-is-one-line-and-four-dependencies.md +70 -0
  43. data/docs/blog/two-forms-one-dom-id.md +69 -0
  44. data/docs/blog/whats-new-async-kanban-wizards.md +130 -0
  45. data/docs/getting-started/tutorial/04-authorization.md +12 -3
  46. data/docs/getting-started/tutorial/06-nested-resources.md +3 -1
  47. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  48. data/docs/guides/authentication.md +73 -0
  49. data/docs/guides/authorization.md +2 -0
  50. data/docs/guides/creating-packages.md +5 -3
  51. data/docs/guides/custom-actions.md +74 -12
  52. data/docs/guides/customizing-ui.md +9 -2
  53. data/docs/guides/index.md +1 -0
  54. data/docs/guides/kanban.md +7 -5
  55. data/docs/guides/nested-resources.md +11 -1
  56. data/docs/guides/performance.md +104 -0
  57. data/docs/guides/user-invites.md +1 -1
  58. data/docs/guides/wizards.md +9 -1
  59. data/docs/index.md +3 -3
  60. data/docs/public/images/home/tour-actions.png +0 -0
  61. data/docs/public/images/home/tour-async.png +0 -0
  62. data/docs/public/images/home/tour-kanban.png +0 -0
  63. data/docs/public/images/home/tour-tenancy.png +0 -0
  64. data/docs/public/images/home/tour-wizard.png +0 -0
  65. data/docs/public/images/reference/async-progress-page.png +0 -0
  66. data/docs/public/images/reference/async-running-banner.png +0 -0
  67. data/docs/public/templates/experimental.rb +34 -0
  68. data/docs/public/templates/pluton8.rb +14 -0
  69. data/docs/reference/app/portals.md +15 -3
  70. data/docs/reference/auth/accounts.md +19 -0
  71. data/docs/reference/behavior/async-interactions.md +295 -0
  72. data/docs/reference/behavior/controllers.md +17 -4
  73. data/docs/reference/behavior/index.md +7 -1
  74. data/docs/reference/behavior/interactions.md +152 -22
  75. data/docs/reference/configuration.md +5 -0
  76. data/docs/reference/index.md +1 -0
  77. data/docs/reference/kanban/dsl.md +7 -4
  78. data/docs/reference/kanban/index.md +1 -1
  79. data/docs/reference/kanban/positioning.md +26 -4
  80. data/docs/reference/positioning.md +568 -0
  81. data/docs/reference/resource/actions.md +97 -4
  82. data/docs/reference/resource/definition.md +181 -9
  83. data/docs/reference/tenancy/invites.md +1 -1
  84. data/docs/reference/tenancy/nested-resources.md +60 -2
  85. data/docs/reference/ui/assets.md +4 -0
  86. data/docs/reference/ui/components.md +57 -4
  87. data/docs/reference/ui/displays.md +20 -10
  88. data/docs/reference/ui/index.md +1 -1
  89. data/docs/reference/wizard/dsl.md +33 -0
  90. data/docs/reference/wizard/storage-config.md +1 -0
  91. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md +624 -0
  92. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md.tasks.json +32 -0
  93. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md +1787 -0
  94. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md.tasks.json +91 -0
  95. data/docs/superpowers/plans/2026-08-17-async-interactions.md +1414 -0
  96. data/docs/superpowers/plans/2026-08-17-async-interactions.md.tasks.json +66 -0
  97. data/docs/superpowers/specs/2026-07-16-homepage-depth-upgrade-design.md +111 -0
  98. data/docs/superpowers/specs/2026-07-17-action-html-attributes-design.md +124 -0
  99. data/docs/superpowers/specs/2026-07-31-positioned-drag-and-drop-design.md +506 -0
  100. data/docs/superpowers/specs/2026-08-17-async-interactions-design.md +185 -0
  101. data/gemfiles/postgres.gemfile.lock +85 -85
  102. data/gemfiles/rails_7.gemfile.lock +322 -140
  103. data/gemfiles/rails_8.0.gemfile.lock +125 -115
  104. data/gemfiles/rails_8.1.gemfile.lock +126 -116
  105. data/lib/generators/pu/async_interactions/install_generator.rb +111 -0
  106. data/lib/generators/pu/async_interactions/templates/app/controllers/async_runs_controller.rb.tt +15 -0
  107. data/lib/generators/pu/core/typespec/typespec_generator.rb +7 -4
  108. data/lib/generators/pu/invites/install_generator.rb +3 -3
  109. data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +2 -2
  110. data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +47 -2
  111. data/lib/generators/pu/lib/plutonium_generators/concerns/resource_registration.rb +41 -0
  112. data/lib/generators/pu/lite/litestream/litestream_generator.rb +1 -1
  113. data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +1 -1
  114. data/lib/generators/pu/res/conn/conn_generator.rb +19 -39
  115. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +4 -0
  116. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +15 -6
  117. data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +7 -0
  118. data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +2 -2
  119. data/lib/generators/pu/wizards/install_generator.rb +78 -0
  120. data/lib/plutonium/action/base.rb +71 -9
  121. data/lib/plutonium/action/interactive.rb +9 -0
  122. data/lib/plutonium/attachments.rb +254 -0
  123. data/lib/plutonium/configuration.rb +82 -1
  124. data/lib/plutonium/core/controller.rb +50 -7
  125. data/lib/plutonium/core/controllers/authorizable.rb +16 -0
  126. data/lib/plutonium/core/controllers/entity_scoping.rb +12 -2
  127. data/lib/plutonium/definition/base.rb +51 -0
  128. data/lib/plutonium/definition/display_layout.rb +112 -0
  129. data/lib/plutonium/definition/index_views.rb +8 -7
  130. data/lib/plutonium/definition/input_aliases.rb +38 -0
  131. data/lib/plutonium/definition/page_widths.rb +65 -0
  132. data/lib/plutonium/definition/positioning.rb +126 -0
  133. data/lib/plutonium/definition/sorting.rb +17 -2
  134. data/lib/plutonium/helpers/turbo_helper.rb +7 -0
  135. data/lib/plutonium/interaction/README.md +61 -24
  136. data/lib/plutonium/interaction/async/configuration.rb +38 -0
  137. data/lib/plutonium/interaction/async/context.rb +419 -0
  138. data/lib/plutonium/interaction/async/executor.rb +422 -0
  139. data/lib/plutonium/interaction/async/job.rb +80 -0
  140. data/lib/plutonium/interaction/async/reap_job.rb +81 -0
  141. data/lib/plutonium/interaction/async/run.rb +394 -0
  142. data/lib/plutonium/interaction/async/run_definition.rb +155 -0
  143. data/lib/plutonium/interaction/async/run_policy.rb +86 -0
  144. data/lib/plutonium/interaction/base.rb +34 -7
  145. data/lib/plutonium/interaction/concerns/dispatchable.rb +518 -0
  146. data/lib/plutonium/interaction/concerns/scoping.rb +70 -9
  147. data/lib/plutonium/interaction/response/redirect.rb +11 -3
  148. data/lib/plutonium/kanban/board.rb +14 -0
  149. data/lib/plutonium/kanban/column.rb +4 -2
  150. data/lib/plutonium/kanban/dsl.rb +4 -1
  151. data/lib/plutonium/kanban/grouping.rb +9 -22
  152. data/lib/plutonium/kanban/positioning.rb +5 -65
  153. data/lib/plutonium/positioning/config.rb +94 -0
  154. data/lib/plutonium/positioning/model.rb +128 -0
  155. data/lib/plutonium/positioning.rb +25 -86
  156. data/lib/plutonium/railtie.rb +1 -0
  157. data/lib/plutonium/resource/controller.rb +118 -38
  158. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +32 -2
  159. data/lib/plutonium/resource/controllers/crud_actions.rb +30 -2
  160. data/lib/plutonium/resource/controllers/eager_loading.rb +87 -0
  161. data/lib/plutonium/resource/controllers/export_csv.rb +10 -1
  162. data/lib/plutonium/resource/controllers/kanban_actions.rb +53 -14
  163. data/lib/plutonium/resource/controllers/position_actions.rb +390 -0
  164. data/lib/plutonium/resource/controllers/presentable.rb +19 -13
  165. data/lib/plutonium/resource/controllers/queryable.rb +5 -1
  166. data/lib/plutonium/resource/controllers/wizard_actions.rb +21 -0
  167. data/lib/plutonium/resource/policy.rb +33 -0
  168. data/lib/plutonium/resource/query_object.rb +36 -0
  169. data/lib/plutonium/routing/mapper_extensions.rb +100 -8
  170. data/lib/plutonium/routing/route_set_extensions.rb +15 -1
  171. data/lib/plutonium/routing/wizard_registration.rb +4 -0
  172. data/lib/plutonium/testing/resource_policy.rb +6 -2
  173. data/lib/plutonium/ui/action_button.rb +12 -7
  174. data/lib/plutonium/ui/actions_dropdown.rb +1 -1
  175. data/lib/plutonium/ui/block.rb +21 -1
  176. data/lib/plutonium/ui/breadcrumbs.rb +187 -55
  177. data/lib/plutonium/ui/component/methods.rb +5 -0
  178. data/lib/plutonium/ui/component/positionable.rb +112 -0
  179. data/lib/plutonium/ui/component/resolves_tags.rb +57 -0
  180. data/lib/plutonium/ui/component/section.rb +185 -0
  181. data/lib/plutonium/ui/display/base.rb +13 -1
  182. data/lib/plutonium/ui/display/components/formatted_value.rb +26 -0
  183. data/lib/plutonium/ui/display/components/section.rb +18 -0
  184. data/lib/plutonium/ui/display/resource.rb +141 -22
  185. data/lib/plutonium/ui/display/theme.rb +20 -1
  186. data/lib/plutonium/ui/export_button.rb +1 -1
  187. data/lib/plutonium/ui/form/base.rb +8 -7
  188. data/lib/plutonium/ui/form/components/intl_tel_input.rb +1 -1
  189. data/lib/plutonium/ui/form/components/section.rb +7 -62
  190. data/lib/plutonium/ui/form/components/uppy.rb +12 -1
  191. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +16 -3
  192. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -1
  193. data/lib/plutonium/ui/form/query.rb +2 -4
  194. data/lib/plutonium/ui/form/resource.rb +92 -15
  195. data/lib/plutonium/ui/form/theme.rb +17 -0
  196. data/lib/plutonium/ui/form/wizard.rb +25 -1
  197. data/lib/plutonium/ui/grid/card.rb +79 -11
  198. data/lib/plutonium/ui/grid/resource.rb +47 -5
  199. data/lib/plutonium/ui/interaction/async/run_progress.rb +227 -0
  200. data/lib/plutonium/ui/interaction/async/running_banner.rb +65 -0
  201. data/lib/plutonium/ui/kanban/card.rb +2 -1
  202. data/lib/plutonium/ui/kanban/column.rb +12 -6
  203. data/lib/plutonium/ui/kanban/resource.rb +6 -7
  204. data/lib/plutonium/ui/layout/base.rb +10 -3
  205. data/lib/plutonium/ui/nav_grid_menu.rb +1 -0
  206. data/lib/plutonium/ui/page/base.rb +19 -0
  207. data/lib/plutonium/ui/page/edit.rb +4 -1
  208. data/lib/plutonium/ui/page/index.rb +69 -18
  209. data/lib/plutonium/ui/page/interactive_action.rb +5 -1
  210. data/lib/plutonium/ui/page/new.rb +4 -1
  211. data/lib/plutonium/ui/page/show.rb +27 -10
  212. data/lib/plutonium/ui/page/wizard.rb +10 -1
  213. data/lib/plutonium/ui/page/wizard_chooser.rb +36 -11
  214. data/lib/plutonium/ui/page_width.rb +58 -0
  215. data/lib/plutonium/ui/table/base.rb +34 -1
  216. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  217. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +32 -8
  218. data/lib/plutonium/ui/table/components/drag_handle.rb +120 -0
  219. data/lib/plutonium/ui/table/components/filter_form.rb +1 -4
  220. data/lib/plutonium/ui/table/components/filter_pills.rb +1 -1
  221. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  222. data/lib/plutonium/ui/table/resource.rb +50 -5
  223. data/lib/plutonium/ui/table/theme.rb +59 -2
  224. data/lib/plutonium/ui/wizard/review.rb +4 -2
  225. data/lib/plutonium/ui/wizard/summary_display.rb +42 -14
  226. data/lib/plutonium/version.rb +1 -1
  227. data/lib/plutonium/wizard/attachments.rb +32 -197
  228. data/lib/plutonium/wizard/base.rb +6 -1
  229. data/lib/plutonium/wizard/configuration.rb +12 -0
  230. data/lib/plutonium/wizard/controller.rb +14 -0
  231. data/lib/plutonium/wizard/driving.rb +99 -15
  232. data/lib/plutonium/wizard/dsl.rb +23 -0
  233. data/lib/plutonium/wizard/resume.rb +127 -49
  234. data/lib/plutonium/wizard/runner.rb +46 -2
  235. data/lib/plutonium/wizard/step_adapter.rb +1 -1
  236. data/lib/plutonium/wizard/sweep_job.rb +16 -0
  237. data/lib/plutonium.rb +21 -0
  238. data/lib/rodauth/features/session_isolation.rb +92 -0
  239. data/lib/rodauth/plugins.rb +1 -0
  240. data/package.json +2 -1
  241. data/plutonium.gemspec +29 -11
  242. data/src/css/components.css +89 -1
  243. data/src/css/slim_select.css +20 -0
  244. data/src/js/controllers/breadcrumbs_controller.js +112 -0
  245. data/src/js/controllers/bulk_actions_controller.js +10 -2
  246. data/src/js/controllers/kanban_controller.js +30 -21
  247. data/src/js/controllers/positioned_controller.js +452 -0
  248. data/src/js/controllers/register_controllers.js +6 -0
  249. data/src/js/controllers/resource_drop_down_controller.js +5 -0
  250. data/src/js/controllers/run_progress_controller.js +73 -0
  251. data/src/js/drag/sortable.js +186 -0
  252. data/yarn.lock +108 -63
  253. metadata +103 -16
  254. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +0 -53
  255. data/docs/.vitepress/theme/components/HomePillars.vue +0 -42
@@ -0,0 +1,185 @@
1
+ # Async interactions — design
2
+
3
+ Status: design approved in conversation, not yet implemented.
4
+
5
+ ## The problem
6
+
7
+ `Plutonium::Resource::Interaction` runs inline. `Interaction.call` validates, runs
8
+ `execute`, and returns an `Outcome` the controller turns into a `Response` — all
9
+ inside one request. That is correct for the operations it was built for, and
10
+ wrong for three kinds of work an app inevitably grows:
11
+
12
+ - **Bulk over many records.** Archiving 500 projects in a request either times
13
+ out or holds a web worker hostage.
14
+ - **One slow operation.** Rebuilding an index, generating a report, calling a
15
+ third-party API. Duration is opaque and often exceeds a request budget.
16
+ - **Anything worth auditing.** Nothing records who ran what, with which options,
17
+ or what it did. The interaction leaves no trace.
18
+
19
+ There is no `perform_later` anywhere in `lib/plutonium` outside
20
+ `Wizard::SweepJob`, so this is genuinely absent rather than partially built.
21
+
22
+ ## The idea
23
+
24
+ **The run becomes a record.** That single change buys everything else:
25
+ persistence outlives the request, so async is natural; a row has a lifecycle, so
26
+ progress and scheduling follow; it is queryable, so audit is free; and it is a
27
+ resource, so it gets an index and show page from machinery that already exists.
28
+
29
+ Prior art is Bullet Train's Action Models. The adaptation below is deliberately
30
+ not a port — Plutonium already owns primitives (policy scopes, entity scoping,
31
+ interactions, `Kanban::Broadcaster`) that Bullet Train has to reinvent.
32
+
33
+ **Interactions dispatch; runs execute.** The interaction keeps its current job —
34
+ declare inputs, validate, authorize, render a form. On `execute` it creates a run
35
+ and enqueues it. It still succeeds *synchronously*, because dispatching is what
36
+ succeeded, so `Outcome` needs no new state. Its response is a redirect to the
37
+ run.
38
+
39
+ ## Architecture
40
+
41
+ ### Storage — one table, STI
42
+
43
+ A single `plutonium_interaction_runs` table with a `type` column. Authors
44
+ subclass to configure behaviour; options live in a JSON column, so an async
45
+ action costs zero migrations.
46
+
47
+ | Column | Purpose |
48
+ |---|---|
49
+ | `type` | STI discriminator — the author's run subclass |
50
+ | `state` | `pending` / `running` / `completed` / `failed` |
51
+ | `options` | JSON. The dispatching interaction's validated inputs |
52
+ | `target_type` | Class name of the targeted resource; `nil` for opaque runs |
53
+ | `target_ids` | JSON array of ids selected at dispatch |
54
+ | `initiator_type` / `initiator_id` | Polymorphic — who started it |
55
+ | `scoped_entity_type` / `scoped_entity_id` | Polymorphic, nullable — tenant context |
56
+ | `progress_total` / `progress_done` | Counts. Both `nil` for opaque work |
57
+ | `errors_log` | JSON. Per-target failures |
58
+ | `started_at` / `finished_at` | Timing, and the basis for sweeping |
59
+
60
+ The shape follows `Wizard::Session`, which is already a persisted process record
61
+ with polymorphic `owner` / `anchor` / `scope` refs and a `SweepJob`. This is not
62
+ a new architecture for the codebase; it is the second instance of one that works.
63
+
64
+ ### Two run shapes
65
+
66
+ The framework picks by what the subclass implements — no mode flag.
67
+
68
+ ```ruby
69
+ class ArchiveProjectsRun < Plutonium::Interaction::Run
70
+ on_failure :continue
71
+
72
+ # Targets arrive from the dispatching interaction's selection. The framework
73
+ # re-resolves them through the policy scope before each call.
74
+ def perform_on(project) = project.archive(notify: options[:notify_users])
75
+ end
76
+
77
+ class RebuildSearchIndexRun < Plutonium::Interaction::Run
78
+ # No targets: one opaque unit, indeterminate progress.
79
+ def perform = SearchIndex.rebuild!
80
+ end
81
+ ```
82
+
83
+ There is deliberately **no author-defined `targets` method**. Bullet Train needs
84
+ `valid_targets` because its action model receives raw ids with nothing to
85
+ constrain them. Plutonium already has that primitive: a bulk action's records
86
+ come from the selection, and `relation_scope` answers "which records may this
87
+ user touch". A second declaration would be a second place to disagree with the
88
+ policy.
89
+
90
+ ### Identifying targets — ids, not GlobalIDs
91
+
92
+ `target_type` (a real column) plus a JSON array of ids. Not GIDs:
93
+
94
+ 1. **The index feature needs a real column.** "Runs in progress for this
95
+ resource" is `where(target_type: Project.name, state: :running)`. That cannot
96
+ be indexed out of a JSON array of `gid://app/Project/1` strings.
97
+ 2. **One scoped query, not N locates.** `policy_scope(Project).where(id: ids)`
98
+ re-resolves through `relation_scope` in a single query and enforces
99
+ authorization by construction. `GlobalID::Locator` bypasses the policy scope
100
+ entirely, requiring a separate re-authorization per record.
101
+ 3. **GIDs solve heterogeneity that does not exist here.** Bulk selection is
102
+ always one resource type; the existing route is already `ids[]=1&ids[]=2`.
103
+
104
+ GlobalID-style polymorphism is still right for `initiator` and `scoped_entity`,
105
+ which genuinely vary by portal — hence the polymorphic columns.
106
+
107
+ ### Authorization outside the request
108
+
109
+ This is the sharpest correctness issue in the design.
110
+
111
+ `perform` runs in a job. There is no `current_user`, no request, no ambient
112
+ context. Plutonium authorizes on the **pair** `(user, entity_scope)` —
113
+ `Authorizable` registers `authorize :entity_scope, through:
114
+ :entity_scope_for_authorize`, and PR #74 exists precisely because a nil entity
115
+ leaking into that memo broke scoping.
116
+
117
+ Therefore:
118
+
119
+ - The run persists **both** `initiator` and `scoped_entity`. Storing only the
120
+ user would re-resolve targets under the wrong tenant — and that fails *open*,
121
+ because a broader scope silently includes records the initiator could not see
122
+ when they dispatched.
123
+ - The job **rebuilds** the context from those columns. It never inherits one.
124
+ - Authorization is **re-checked at perform time**, not trusted from dispatch.
125
+ Access revoked between enqueue and run must stop the work, or a persisted run
126
+ becomes a way to launder stale permissions.
127
+
128
+ **Decision: `initiator` is required.** A nullable column would silently come to
129
+ mean "unscoped", which is the failure mode above. System-triggered runs
130
+ (schedules, callbacks) should get an explicit system initiator later rather than
131
+ a null.
132
+
133
+ ### Failure policy
134
+
135
+ Configured on the run subclass, since the author knows the semantics:
136
+
137
+ - `on_failure :continue` — record the failure in `errors_log`, keep going.
138
+ Correct for bulk, where 497 successes should not be discarded.
139
+ - `on_failure :halt` — stop at the first error.
140
+ - `on_failure :transactional` — wrap the whole run in one transaction. Honest
141
+ about its cost: a long transaction over many records risks lock contention,
142
+ and is impossible once the work touches a third-party API.
143
+
144
+ **Decision: a target that vanishes or falls out of scope between dispatch and
145
+ perform is recorded as a per-target failure, not skipped.** "3 targets were no
146
+ longer available" is information the operator needs; silence there is how bulk
147
+ operations quietly under-apply.
148
+
149
+ ### Progress transport
150
+
151
+ **Turbo poll by default, broadcast opt-in.** The progress page is a turbo-frame
152
+ with a refresh interval — no ActionCable, works on any deployment. A run may opt
153
+ into realtime and reuse `Kanban::Broadcaster`, whose stream names are already
154
+ keyed on **both** `resource_class` and `scoped_entity` so tenants can never share
155
+ a stream. This matches the house posture: kanban's `realtime` defaults to false.
156
+
157
+ ### Surfaces
158
+
159
+ - **The run is a resource.** The framework ships a definition and policy, so a
160
+ portal can register it and get index and show pages. The progress page is
161
+ simply the run's show page — no bespoke view.
162
+ - **Dispatch redirects** to that show page, via the interaction's ordinary
163
+ `Response::Redirect`.
164
+ - **The target resource's index lists in-progress runs at the top**, queried by
165
+ `target_type` — which is why it is a column rather than JSON.
166
+
167
+ ## Testing
168
+
169
+ - **Unit** — state machine, failure policies, progress accounting, options
170
+ round-tripping through JSON.
171
+ - **Authorization** — the highest-value tests. A run whose initiator lost access
172
+ between dispatch and perform must not perform. A run must not resolve targets
173
+ outside its stored `scoped_entity`. Both fail open if wrong, so both need
174
+ explicit coverage.
175
+ - **Integration** — dispatch returns a redirect; the show page reports progress;
176
+ the target index lists running work.
177
+ - **System** — one end-to-end pass on the progress page updating.
178
+
179
+ ## Out of scope
180
+
181
+ Scheduling (`run_at`), approval workflows, retries, and import/export run types.
182
+ All are natural extensions of a persisted run and none is needed to make the
183
+ first version useful. Retention/sweeping follows `Wizard::SweepJob`'s pattern
184
+ when it is needed; runs are kept indefinitely until then, since audit is a
185
+ motivation.
@@ -3,29 +3,29 @@ GEM
3
3
  specs:
4
4
  action_text-trix (2.1.19)
5
5
  railties
6
- actioncable (8.1.3)
7
- actionpack (= 8.1.3)
8
- activesupport (= 8.1.3)
6
+ actioncable (8.1.3.1)
7
+ actionpack (= 8.1.3.1)
8
+ activesupport (= 8.1.3.1)
9
9
  nio4r (~> 2.0)
10
10
  websocket-driver (>= 0.6.1)
11
11
  zeitwerk (~> 2.6)
12
- actionmailbox (8.1.3)
13
- actionpack (= 8.1.3)
14
- activejob (= 8.1.3)
15
- activerecord (= 8.1.3)
16
- activestorage (= 8.1.3)
17
- activesupport (= 8.1.3)
12
+ actionmailbox (8.1.3.1)
13
+ actionpack (= 8.1.3.1)
14
+ activejob (= 8.1.3.1)
15
+ activerecord (= 8.1.3.1)
16
+ activestorage (= 8.1.3.1)
17
+ activesupport (= 8.1.3.1)
18
18
  mail (>= 2.8.0)
19
- actionmailer (8.1.3)
20
- actionpack (= 8.1.3)
21
- actionview (= 8.1.3)
22
- activejob (= 8.1.3)
23
- activesupport (= 8.1.3)
19
+ actionmailer (8.1.3.1)
20
+ actionpack (= 8.1.3.1)
21
+ actionview (= 8.1.3.1)
22
+ activejob (= 8.1.3.1)
23
+ activesupport (= 8.1.3.1)
24
24
  mail (>= 2.8.0)
25
25
  rails-dom-testing (~> 2.2)
26
- actionpack (8.1.3)
27
- actionview (= 8.1.3)
28
- activesupport (= 8.1.3)
26
+ actionpack (8.1.3.1)
27
+ actionview (= 8.1.3.1)
28
+ activesupport (= 8.1.3.1)
29
29
  nokogiri (>= 1.8.5)
30
30
  rack (>= 2.2.4)
31
31
  rack-session (>= 1.0.1)
@@ -33,36 +33,36 @@ GEM
33
33
  rails-dom-testing (~> 2.2)
34
34
  rails-html-sanitizer (~> 1.6)
35
35
  useragent (~> 0.16)
36
- actiontext (8.1.3)
36
+ actiontext (8.1.3.1)
37
37
  action_text-trix (~> 2.1.15)
38
- actionpack (= 8.1.3)
39
- activerecord (= 8.1.3)
40
- activestorage (= 8.1.3)
41
- activesupport (= 8.1.3)
38
+ actionpack (= 8.1.3.1)
39
+ activerecord (= 8.1.3.1)
40
+ activestorage (= 8.1.3.1)
41
+ activesupport (= 8.1.3.1)
42
42
  globalid (>= 0.6.0)
43
43
  nokogiri (>= 1.8.5)
44
- actionview (8.1.3)
45
- activesupport (= 8.1.3)
44
+ actionview (8.1.3.1)
45
+ activesupport (= 8.1.3.1)
46
46
  builder (~> 3.1)
47
47
  erubi (~> 1.11)
48
48
  rails-dom-testing (~> 2.2)
49
49
  rails-html-sanitizer (~> 1.6)
50
- activejob (8.1.3)
51
- activesupport (= 8.1.3)
50
+ activejob (8.1.3.1)
51
+ activesupport (= 8.1.3.1)
52
52
  globalid (>= 0.3.6)
53
- activemodel (8.1.3)
54
- activesupport (= 8.1.3)
55
- activerecord (8.1.3)
56
- activemodel (= 8.1.3)
57
- activesupport (= 8.1.3)
53
+ activemodel (8.1.3.1)
54
+ activesupport (= 8.1.3.1)
55
+ activerecord (8.1.3.1)
56
+ activemodel (= 8.1.3.1)
57
+ activesupport (= 8.1.3.1)
58
58
  timeout (>= 0.4.0)
59
- activestorage (8.1.3)
60
- actionpack (= 8.1.3)
61
- activejob (= 8.1.3)
62
- activerecord (= 8.1.3)
63
- activesupport (= 8.1.3)
59
+ activestorage (8.1.3.1)
60
+ actionpack (= 8.1.3.1)
61
+ activejob (= 8.1.3.1)
62
+ activerecord (= 8.1.3.1)
63
+ activesupport (= 8.1.3.1)
64
64
  marcel (~> 1.0)
65
- activesupport (8.1.3)
65
+ activesupport (8.1.3.1)
66
66
  base64
67
67
  bigdecimal
68
68
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -78,12 +78,12 @@ GEM
78
78
  base64 (0.3.0)
79
79
  bigdecimal (4.1.2)
80
80
  builder (3.3.0)
81
- concurrent-ruby (1.3.7)
81
+ concurrent-ruby (1.3.8)
82
82
  connection_pool (3.0.2)
83
83
  crass (1.0.7)
84
84
  date (3.5.1)
85
85
  drb (2.2.3)
86
- erb (6.0.4)
86
+ erb (6.0.6)
87
87
  erubi (1.13.1)
88
88
  globalid (1.4.0)
89
89
  activesupport (>= 6.1)
@@ -95,12 +95,12 @@ GEM
95
95
  prism (>= 1.3.0)
96
96
  rdoc (>= 4.0.0)
97
97
  reline (>= 0.4.2)
98
- json (2.20.0)
98
+ json (2.21.2)
99
99
  logger (1.7.0)
100
- loofah (2.25.1)
100
+ loofah (2.25.2)
101
101
  crass (~> 1.0.2)
102
102
  nokogiri (>= 1.12.0)
103
- mail (2.9.0)
103
+ mail (2.9.1)
104
104
  logger
105
105
  mini_mime (>= 0.1.1)
106
106
  net-imap
@@ -112,7 +112,7 @@ GEM
112
112
  minitest (6.0.6)
113
113
  drb (~> 2.0)
114
114
  prism (~> 1.5)
115
- net-imap (0.6.4.1)
115
+ net-imap (0.6.6)
116
116
  date
117
117
  net-protocol
118
118
  net-pop (0.1.2)
@@ -161,30 +161,30 @@ GEM
161
161
  rack (>= 1.3)
162
162
  rackup (2.3.1)
163
163
  rack (>= 3)
164
- rails (8.1.3)
165
- actioncable (= 8.1.3)
166
- actionmailbox (= 8.1.3)
167
- actionmailer (= 8.1.3)
168
- actionpack (= 8.1.3)
169
- actiontext (= 8.1.3)
170
- actionview (= 8.1.3)
171
- activejob (= 8.1.3)
172
- activemodel (= 8.1.3)
173
- activerecord (= 8.1.3)
174
- activestorage (= 8.1.3)
175
- activesupport (= 8.1.3)
164
+ rails (8.1.3.1)
165
+ actioncable (= 8.1.3.1)
166
+ actionmailbox (= 8.1.3.1)
167
+ actionmailer (= 8.1.3.1)
168
+ actionpack (= 8.1.3.1)
169
+ actiontext (= 8.1.3.1)
170
+ actionview (= 8.1.3.1)
171
+ activejob (= 8.1.3.1)
172
+ activemodel (= 8.1.3.1)
173
+ activerecord (= 8.1.3.1)
174
+ activestorage (= 8.1.3.1)
175
+ activesupport (= 8.1.3.1)
176
176
  bundler (>= 1.15.0)
177
- railties (= 8.1.3)
177
+ railties (= 8.1.3.1)
178
178
  rails-dom-testing (2.3.0)
179
179
  activesupport (>= 5.0.0)
180
180
  minitest
181
181
  nokogiri (>= 1.6)
182
- rails-html-sanitizer (1.7.0)
183
- loofah (~> 2.25)
182
+ rails-html-sanitizer (1.7.1)
183
+ loofah (~> 2.25, >= 2.25.2)
184
184
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
185
- railties (8.1.3)
186
- actionpack (= 8.1.3)
187
- activesupport (= 8.1.3)
185
+ railties (8.1.3.1)
186
+ actionpack (= 8.1.3.1)
187
+ activesupport (= 8.1.3.1)
188
188
  irb (~> 1.13)
189
189
  rackup (>= 1.0.0)
190
190
  rake (>= 12.2)
@@ -192,7 +192,7 @@ GEM
192
192
  tsort (>= 0.2)
193
193
  zeitwerk (~> 2.6)
194
194
  rake (13.4.2)
195
- rbs (4.0.3)
195
+ rbs (4.1.1)
196
196
  logger
197
197
  prism (>= 1.6.0)
198
198
  tsort
@@ -215,7 +215,7 @@ GEM
215
215
  base64
216
216
  websocket-extensions (>= 0.1.0)
217
217
  websocket-extensions (0.1.5)
218
- zeitwerk (2.8.2)
218
+ zeitwerk (2.8.3)
219
219
 
220
220
  PLATFORMS
221
221
  aarch64-linux
@@ -236,40 +236,40 @@ DEPENDENCIES
236
236
 
237
237
  CHECKSUMS
238
238
  action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009
239
- actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0
240
- actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59
241
- actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d
242
- actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3
243
- actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2
244
- actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d
245
- activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3
246
- activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219
247
- activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
248
- activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d
249
- activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
239
+ actioncable (8.1.3.1) sha256=e318528295c878a3efdfe25f0f2267c80cb7a76eba41bb5f64d44aa380a3d91b
240
+ actionmailbox (8.1.3.1) sha256=5f704972097d843ade8e435e93694a1dac732b926df1717aceba1f3840082b1c
241
+ actionmailer (8.1.3.1) sha256=88ea441b28ff02a0c6c006468892642a3d9942affce9d294e81a74504aa5c43c
242
+ actionpack (8.1.3.1) sha256=974cb7154548e81f470b1b0f247b99cb38e87825899dca58610596e2817723d0
243
+ actiontext (8.1.3.1) sha256=5da729d833d1a29cddb1eee938878e55e503d2613e00e735f5daf58c2ba98af2
244
+ actionview (8.1.3.1) sha256=2da68b8414c47b43bfbed1ce69c5afe1c04f78c267aacb5660a4cab5ca12cfb6
245
+ activejob (8.1.3.1) sha256=1c8dd275df930df40deecffec63d913a550a33fd94bd298f69721dd96939954a
246
+ activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88
247
+ activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b
248
+ activestorage (8.1.3.1) sha256=f555254f387b1cffa499d2fd3115d12635eadc5b15206a8534316a67036163ef
249
+ activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376
250
250
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
251
251
  bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
252
252
  builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
253
- concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
253
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
254
254
  connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
255
255
  crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
256
256
  date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
257
257
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
258
- erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
258
+ erb (6.0.6) sha256=a9b24986700f5bf127c4f297c5403c3ca41b83b0a316c0cd09a096b56e644ae5
259
259
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
260
260
  globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427
261
261
  i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
262
262
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
263
263
  irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
264
- json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b
264
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
265
265
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
266
- loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04
267
- mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
266
+ loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918
267
+ mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8
268
268
  marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f
269
269
  mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
270
270
  mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
271
271
  minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
272
- net-imap (0.6.4.1) sha256=29f0360d75a7efd3539f16ac1957dea5c0a51ddeceb348db4553c3120914ea0d
272
+ net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df
273
273
  net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
274
274
  net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
275
275
  net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
@@ -298,12 +298,12 @@ CHECKSUMS
298
298
  rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
299
299
  rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
300
300
  rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
301
- rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3
301
+ rails (8.1.3.1) sha256=ccd11a36bfc171bf9c66d585d14c0ece91c0c9dde840aae60c0118d6f5c9c52a
302
302
  rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
303
- rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89
304
- railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22
303
+ rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2
304
+ railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487
305
305
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
306
- rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
306
+ rbs (4.1.1) sha256=1bf118f2f95d1cd3956357645d495152b661e0257e57781d18ceecd461622b9e
307
307
  rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
308
308
  reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
309
309
  securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
@@ -315,7 +315,7 @@ CHECKSUMS
315
315
  useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
316
316
  websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146
317
317
  websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
318
- zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
318
+ zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
319
319
 
320
320
  BUNDLED WITH
321
321
  4.0.6