plutonium 0.60.5 → 0.62.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 (199) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +41 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +40 -0
  5. data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
  6. data/.claude/skills/plutonium-kanban/SKILL.md +378 -0
  7. data/.claude/skills/plutonium-resource/SKILL.md +40 -0
  8. data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
  9. data/.claude/skills/plutonium-testing/SKILL.md +38 -0
  10. data/.claude/skills/plutonium-ui/SKILL.md +51 -0
  11. data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
  12. data/.cliff.toml +6 -0
  13. data/Appraisals +3 -0
  14. data/CHANGELOG.md +576 -439
  15. data/CLAUDE.md +15 -7
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +1173 -194
  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/layouts/basic.html.erb +7 -0
  22. data/app/views/plutonium/_flash_toasts.html.erb +2 -46
  23. data/app/views/plutonium/_toast.html.erb +52 -0
  24. data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
  25. data/app/views/resource/_resource_kanban.html.erb +1 -0
  26. data/app/views/resource/kanban_move_form.html.erb +1 -0
  27. data/config/brakeman.ignore +2 -2
  28. data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
  29. data/docs/.vitepress/config.ts +45 -1
  30. data/docs/.vitepress/sync-skills.mjs +45 -0
  31. data/docs/ai.md +99 -0
  32. data/docs/guides/index.md +2 -0
  33. data/docs/guides/kanban.md +557 -0
  34. data/docs/guides/wizards.md +447 -0
  35. data/docs/public/images/guides/kanban-after-move.png +0 -0
  36. data/docs/public/images/guides/kanban-board-light.png +0 -0
  37. data/docs/public/images/guides/kanban-board.png +0 -0
  38. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  39. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  40. data/docs/public/images/guides/wizards-chooser.png +0 -0
  41. data/docs/public/images/guides/wizards-completed.png +0 -0
  42. data/docs/public/images/guides/wizards-index-action.png +0 -0
  43. data/docs/public/images/guides/wizards-repeater.png +0 -0
  44. data/docs/public/images/guides/wizards-review.png +0 -0
  45. data/docs/public/images/guides/wizards-step.png +0 -0
  46. data/docs/reference/behavior/policies.md +1 -1
  47. data/docs/reference/index.md +14 -0
  48. data/docs/reference/kanban/authorization.md +82 -0
  49. data/docs/reference/kanban/dsl.md +334 -0
  50. data/docs/reference/kanban/index.md +40 -0
  51. data/docs/reference/kanban/positioning.md +162 -0
  52. data/docs/reference/resource/definition.md +26 -1
  53. data/docs/reference/resource/model.md +26 -0
  54. data/docs/reference/ui/forms.md +77 -0
  55. data/docs/reference/ui/pages.md +2 -0
  56. data/docs/reference/wizard/anchoring-resume.md +194 -0
  57. data/docs/reference/wizard/dsl.md +337 -0
  58. data/docs/reference/wizard/index.md +33 -0
  59. data/docs/reference/wizard/one-time.md +129 -0
  60. data/docs/reference/wizard/registration-launch.md +177 -0
  61. data/docs/reference/wizard/storage-config.md +151 -0
  62. data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
  63. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
  64. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
  65. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
  66. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
  67. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
  68. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
  69. data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
  70. data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
  71. data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
  72. data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
  73. data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
  74. data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
  75. data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
  76. data/gemfiles/postgres.gemfile +8 -0
  77. data/gemfiles/postgres.gemfile.lock +321 -0
  78. data/gemfiles/rails_7.gemfile +1 -0
  79. data/gemfiles/rails_7.gemfile.lock +1 -1
  80. data/gemfiles/rails_8.0.gemfile +1 -0
  81. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  82. data/gemfiles/rails_8.1.gemfile +1 -0
  83. data/gemfiles/rails_8.1.gemfile.lock +14 -1
  84. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
  85. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
  86. data/lib/plutonium/action/base.rb +17 -0
  87. data/lib/plutonium/auth/rodauth.rb +1 -2
  88. data/lib/plutonium/configuration.rb +16 -0
  89. data/lib/plutonium/core/controller.rb +20 -1
  90. data/lib/plutonium/definition/base.rb +25 -0
  91. data/lib/plutonium/definition/form_layout.rb +54 -35
  92. data/lib/plutonium/definition/index_views.rb +70 -1
  93. data/lib/plutonium/definition/wizards.rb +209 -0
  94. data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
  95. data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
  96. data/lib/plutonium/invites/controller.rb +4 -1
  97. data/lib/plutonium/kanban/action.rb +7 -0
  98. data/lib/plutonium/kanban/board.rb +40 -0
  99. data/lib/plutonium/kanban/broadcaster.rb +54 -0
  100. data/lib/plutonium/kanban/column.rb +122 -0
  101. data/lib/plutonium/kanban/context.rb +15 -0
  102. data/lib/plutonium/kanban/dsl.rb +71 -0
  103. data/lib/plutonium/kanban/grouping.rb +51 -0
  104. data/lib/plutonium/kanban/positioning.rb +75 -0
  105. data/lib/plutonium/kanban.rb +11 -0
  106. data/lib/plutonium/migrations.rb +40 -0
  107. data/lib/plutonium/models/has_cents.rb +30 -2
  108. data/lib/plutonium/positioning.rb +146 -0
  109. data/lib/plutonium/railtie.rb +33 -0
  110. data/lib/plutonium/resource/controller.rb +24 -1
  111. data/lib/plutonium/resource/controllers/crud_actions.rb +9 -1
  112. data/lib/plutonium/resource/controllers/kanban_actions.rb +851 -0
  113. data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
  114. data/lib/plutonium/resource/policy.rb +14 -0
  115. data/lib/plutonium/routing/mapper_extensions.rb +45 -0
  116. data/lib/plutonium/routing/wizard_registration.rb +289 -0
  117. data/lib/plutonium/ui/display/components/currency.rb +41 -9
  118. data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
  119. data/lib/plutonium/ui/display/resource.rb +17 -12
  120. data/lib/plutonium/ui/form/base.rb +25 -5
  121. data/lib/plutonium/ui/form/components/currency.rb +64 -0
  122. data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
  123. data/lib/plutonium/ui/form/components/password.rb +126 -0
  124. data/lib/plutonium/ui/form/components/uppy.rb +26 -5
  125. data/lib/plutonium/ui/form/kanban_move.rb +46 -0
  126. data/lib/plutonium/ui/form/options/inferred_types.rb +26 -0
  127. data/lib/plutonium/ui/form/resource.rb +13 -1
  128. data/lib/plutonium/ui/form/theme.rb +7 -0
  129. data/lib/plutonium/ui/form/wizard.rb +63 -0
  130. data/lib/plutonium/ui/grid/card.rb +56 -18
  131. data/lib/plutonium/ui/kanban/card.rb +67 -0
  132. data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
  133. data/lib/plutonium/ui/kanban/column.rb +411 -0
  134. data/lib/plutonium/ui/kanban/resource.rb +319 -0
  135. data/lib/plutonium/ui/layout/base.rb +1 -1
  136. data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
  137. data/lib/plutonium/ui/modal/base.rb +30 -3
  138. data/lib/plutonium/ui/modal/centered.rb +5 -2
  139. data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
  140. data/lib/plutonium/ui/page/index.rb +2 -1
  141. data/lib/plutonium/ui/page/interactive_action.rb +12 -2
  142. data/lib/plutonium/ui/page/kanban_move.rb +20 -0
  143. data/lib/plutonium/ui/page/show.rb +29 -1
  144. data/lib/plutonium/ui/page/wizard.rb +371 -0
  145. data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
  146. data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
  147. data/lib/plutonium/ui/table/base.rb +1 -1
  148. data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
  149. data/lib/plutonium/ui/table/resource.rb +1 -1
  150. data/lib/plutonium/ui/wizard/review.rb +196 -0
  151. data/lib/plutonium/ui/wizard/stepper.rb +122 -0
  152. data/lib/plutonium/ui/wizard/summary_display.rb +92 -0
  153. data/lib/plutonium/version.rb +1 -1
  154. data/lib/plutonium/wizard/attachment_data.rb +42 -0
  155. data/lib/plutonium/wizard/attachments.rb +226 -0
  156. data/lib/plutonium/wizard/base.rb +216 -0
  157. data/lib/plutonium/wizard/base_controller.rb +31 -0
  158. data/lib/plutonium/wizard/configuration.rb +42 -0
  159. data/lib/plutonium/wizard/controller.rb +162 -0
  160. data/lib/plutonium/wizard/data.rb +134 -0
  161. data/lib/plutonium/wizard/driving.rb +639 -0
  162. data/lib/plutonium/wizard/dsl.rb +336 -0
  163. data/lib/plutonium/wizard/errors.rb +27 -0
  164. data/lib/plutonium/wizard/field_capture.rb +157 -0
  165. data/lib/plutonium/wizard/field_importer.rb +208 -0
  166. data/lib/plutonium/wizard/gate.rb +171 -0
  167. data/lib/plutonium/wizard/instance_key.rb +97 -0
  168. data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
  169. data/lib/plutonium/wizard/resume.rb +250 -0
  170. data/lib/plutonium/wizard/review_step.rb +48 -0
  171. data/lib/plutonium/wizard/route_resolution.rb +40 -0
  172. data/lib/plutonium/wizard/runner.rb +684 -0
  173. data/lib/plutonium/wizard/session.rb +53 -0
  174. data/lib/plutonium/wizard/state.rb +35 -0
  175. data/lib/plutonium/wizard/step.rb +61 -0
  176. data/lib/plutonium/wizard/step_adapter.rb +103 -0
  177. data/lib/plutonium/wizard/store/active_record.rb +174 -0
  178. data/lib/plutonium/wizard/store/base.rb +42 -0
  179. data/lib/plutonium/wizard/store/memory.rb +44 -0
  180. data/lib/plutonium/wizard/sweep_job.rb +76 -0
  181. data/lib/plutonium/wizard.rb +86 -0
  182. data/lib/plutonium.rb +5 -0
  183. data/lib/rodauth/features/case_insensitive_login.rb +1 -1
  184. data/lib/tasks/release.rake +144 -191
  185. data/package.json +7 -5
  186. data/src/css/components.css +137 -0
  187. data/src/js/controllers/attachment_input_controller.js +15 -1
  188. data/src/js/controllers/currency_input_controller.js +39 -0
  189. data/src/js/controllers/dirty_form_guard_controller.js +155 -27
  190. data/src/js/controllers/intl_tel_input_controller.js +4 -0
  191. data/src/js/controllers/kanban_controller.js +717 -0
  192. data/src/js/controllers/password_sentinel_controller.js +39 -0
  193. data/src/js/controllers/register_controllers.js +8 -0
  194. data/src/js/controllers/remote_modal_controller.js +10 -0
  195. data/src/js/controllers/row_click_controller.js +14 -1
  196. data/src/js/controllers/wizard_controller.js +54 -0
  197. data/src/js/turbo/turbo_confirm.js +1 -1
  198. data/yarn.lock +945 -286
  199. metadata +113 -2
@@ -0,0 +1,7 @@
1
+ <%= render Plutonium::UI::Layout::BasicLayout.new do |layout| %>
2
+ <%# Shell-less wizards have no topbar, so lift the content off the viewport edge %>
3
+ <%# and cap its width — keeping it comfortably centered like an onboarding screen. %>
4
+ <div class="mx-auto w-full max-w-3xl pt-8 sm:pt-14">
5
+ <%= yield %>
6
+ </div>
7
+ <% end %>
@@ -1,48 +1,4 @@
1
1
  <% flash.each do |type, msg| %>
2
- <%
3
- next unless msg.present?
4
-
5
- variant = {
6
- success: "success",
7
- warning: "warning",
8
- alert: "danger", danger: "danger", error: "danger"
9
- }.fetch(type.to_sym, "info")
10
- %>
11
-
12
- <div data-controller="resource-dismiss"
13
- data-resource-dismiss-after-value="6000"
14
- class="pu-toast pu-toast-<%= variant %>"
15
- role="alert">
16
-
17
- <div class="pu-toast-icon">
18
- <% case type.to_sym %>
19
- <% when :success %>
20
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
21
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
22
- </svg>
23
- <% when :warning %>
24
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
25
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
26
- </svg>
27
- <% when :alert, :danger, :error %>
28
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
29
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
30
- </svg>
31
- <% else %>
32
- <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
33
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
34
- </svg>
35
- <% end %>
36
- </div>
37
- <div class="pu-toast-message"><%= msg %></div>
38
- <button type="button"
39
- class="pu-toast-close"
40
- data-action="click->resource-dismiss#dismiss"
41
- aria-label="Close">
42
- <span class="sr-only">Close</span>
43
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
44
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
45
- </svg>
46
- </button>
47
- </div>
2
+ <% next unless msg.present? %>
3
+ <%= render "plutonium/toast", type: type, msg: msg %>
48
4
  <% end %>
@@ -0,0 +1,52 @@
1
+ <%#
2
+ Renders a single dismissable toast. Locals:
3
+ type — flash-style key (:success, :warning, :alert/:danger/:error, …)
4
+ msg — the message string
5
+ Shared by _flash_toasts (one per flash entry) and any turbo_stream that
6
+ wants to append a single toast without re-rendering the whole flash set
7
+ (e.g. the kanban move-rejection feedback).
8
+ %>
9
+ <%
10
+ variant = {
11
+ success: "success",
12
+ warning: "warning",
13
+ alert: "danger", danger: "danger", error: "danger"
14
+ }.fetch(type.to_sym, "info")
15
+ %>
16
+
17
+ <div data-controller="resource-dismiss"
18
+ data-resource-dismiss-after-value="6000"
19
+ class="pu-toast pu-toast-<%= variant %>"
20
+ role="alert">
21
+
22
+ <div class="pu-toast-icon">
23
+ <% case type.to_sym %>
24
+ <% when :success %>
25
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
26
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
27
+ </svg>
28
+ <% when :warning %>
29
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
30
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
31
+ </svg>
32
+ <% when :alert, :danger, :error %>
33
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
34
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
35
+ </svg>
36
+ <% else %>
37
+ <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
38
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
39
+ </svg>
40
+ <% end %>
41
+ </div>
42
+ <div class="pu-toast-message"><%= msg %></div>
43
+ <button type="button"
44
+ class="pu-toast-close"
45
+ data-action="click->resource-dismiss#dismiss"
46
+ aria-label="Close">
47
+ <span class="sr-only">Close</span>
48
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
49
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
50
+ </svg>
51
+ </button>
52
+ </div>
@@ -0,0 +1 @@
1
+ <%= render Plutonium::UI::Form::KanbanMove.new(@interaction) %>
@@ -0,0 +1 @@
1
+ <%= render build_kanban_board_shell %>
@@ -0,0 +1 @@
1
+ <%= render Plutonium::UI::Page::KanbanMove.new %>
@@ -144,11 +144,11 @@
144
144
  {
145
145
  "warning_type": "Dangerous Eval",
146
146
  "warning_code": 13,
147
- "fingerprint": "ac51d516bdbc88a0a624795dad1b3efc1d2744b3f2d7be7feb67e0b5cebab763",
147
+ "fingerprint": "bdacd028907815ba0db0cf99e069d1ea7e94a6e527b7746efd97886c52b2f928",
148
148
  "check_name": "Evaluation",
149
149
  "message": "Dynamic string evaluated as code",
150
150
  "file": "lib/plutonium/models/has_cents.rb",
151
- "line": 134,
151
+ "line": 162,
152
152
  "link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
153
153
  "code": "class_eval(\" # Getter method for the decimal representation of the cents value.\\n #\\n # @return [BigDecimal, nil] The decimal value or nil if cents_name is not present.\\n def #{(:\"#{cents_name.to_sym}_#{suffix}\" or cents_name.to_sym.to_s.gsub(/_cents$/, \"\").to_sym)}\\n #{cents_name.to_sym}.to_d / #{rate} if #{cents_name.to_sym}.present?\\n end\\n\\n # Setter method for the decimal representation of the cents value.\\n #\\n # @param value [Numeric, nil] The decimal value to be set.\\n def #{(:\"#{cents_name.to_sym}_#{suffix}\" or cents_name.to_sym.to_s.gsub(/_cents$/, \"\").to_sym)}=(value)\\n self.#{cents_name.to_sym} = begin\\n (BigDecimal(value.to_s) * #{rate}).to_i if value.present?\\n rescue ArgumentError\\n nil\\n end\\n end\\n\\n # Mark decimal field as invalid if cents field is not valid\\n after_validation do\\n next unless errors[#{cents_name.to_sym.inspect}].present?\\n\\n errors.add(#{(:\"#{cents_name.to_sym}_#{suffix}\" or cents_name.to_sym.to_s.gsub(/_cents$/, \"\").to_sym).inspect}, :invalid)\\n end\\n\", \"lib/plutonium/models/has_cents.rb\", 135)",
154
154
  "render_path": null,
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePlutoniumWizardSessions < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :plutonium_wizard_sessions do |t|
6
+ t.string :wizard, null: false
7
+ t.string :status, null: false, default: "in_progress" # in_progress | completing | completed
8
+ t.string :current_step
9
+
10
+ # Identity — a deterministic digest of (wizard, scope, anchor, principal).
11
+ # A single unique column is required because nullable polymorphic columns
12
+ # can't enforce the singleton rule (NULL != NULL in unique indexes).
13
+ t.string :instance_key, null: false
14
+
15
+ # Polymorphic refs — for querying/listing, NOT identity. *_id is string-typed
16
+ # to accommodate bigint or uuid host primary keys.
17
+ t.string :scope_type
18
+ t.string :scope_id
19
+ t.string :owner_type
20
+ t.string :owner_id
21
+ t.string :anchor_type
22
+ t.string :anchor_id
23
+ t.string :token
24
+
25
+ # The portal (engine) this run was launched in, e.g. "OrgPortal::Engine" (the
26
+ # main app's class name for a public mount). Records the run's portal context
27
+ # so the "continue where you left off" listing only ever shows — and links —
28
+ # runs that belong to the portal being viewed (two portals can share an entity
29
+ # scope, so scope alone can't identify the portal).
30
+ t.string :engine
31
+
32
+ # Optimistic-merge version. The runner reads a row at one version and writes
33
+ # it back a request later; a concurrent advance bumps the version in between.
34
+ # The store compares the version the state was read at against the row's
35
+ # current version under a row lock and MERGES (rather than clobbers) when they
36
+ # differ, so two concurrent writers on the same run never lose each other's
37
+ # staged data.
38
+ t.integer :lock_version, null: false, default: 0
39
+
40
+ t.public_send(:jsonb, :data, null: false, default: {})
41
+ t.public_send(:jsonb, :tracked_records, null: false, default: {})
42
+ # Steps the user has actually visited+validated (§6.3 completeness). A
43
+ # zero-validation step is only "complete" once it's been visited.
44
+ t.public_send(:jsonb, :visited, null: false, default: [])
45
+
46
+ t.datetime :expires_at
47
+ t.datetime :completed_at
48
+ t.timestamps
49
+
50
+ t.index :instance_key, unique: true
51
+ t.index [:status, :expires_at]
52
+ t.index [:owner_type, :owner_id, :engine, :status]
53
+ t.index [:scope_type, :scope_id, :status]
54
+ t.index [:wizard, :anchor_type, :anchor_id, :status]
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,6 @@
1
1
  import { defineConfig } from "vitepress"
2
2
  import { withMermaid } from "vitepress-plugin-mermaid";
3
+ import llmstxt from "vitepress-plugin-llms";
3
4
 
4
5
  const base = "/plutonium-core/"
5
6
 
@@ -22,6 +23,24 @@ export default defineConfig(withMermaid({
22
23
  ],
23
24
  ignoreDeadLinks: 'localhostLinks',
24
25
  srcExclude: ['superpowers/**'],
26
+ vite: {
27
+ plugins: [
28
+ // Generates llms.txt, llms-full.txt, and a raw .md twin for every page.
29
+ llmstxt({
30
+ // Site base (/plutonium-core/) is appended automatically — domain must not include it.
31
+ domain: "https://radioactive-labs.github.io",
32
+ // public/ is served verbatim (skills live there); superpowers/ is internal.
33
+ // Section landing pages are Vue components with no markdown content.
34
+ ignoreFiles: [
35
+ "superpowers/**",
36
+ "public/**",
37
+ "getting-started/index.md",
38
+ "guides/index.md",
39
+ "reference/index.md",
40
+ ],
41
+ }),
42
+ ],
43
+ },
25
44
  themeConfig: {
26
45
  // https://vitepress.dev/reference/default-theme-config
27
46
  logo: "/plutonium.png",
@@ -32,7 +51,8 @@ export default defineConfig(withMermaid({
32
51
  { text: "Home", link: "/" },
33
52
  { text: "Getting Started", link: "/getting-started/" },
34
53
  { text: "Guides", link: "/guides/" },
35
- { text: "Reference", link: "/reference/" }
54
+ { text: "Reference", link: "/reference/" },
55
+ { text: "For AI Agents", link: "/ai" }
36
56
  ],
37
57
  sidebar: {
38
58
  '/getting-started/': [
@@ -88,6 +108,8 @@ export default defineConfig(withMermaid({
88
108
  { text: "Multi-tenancy", link: "/guides/multi-tenancy" },
89
109
  { text: "Search & Filtering", link: "/guides/search-filtering" },
90
110
  { text: "User Invites", link: "/guides/user-invites" },
111
+ { text: "Wizards", link: "/guides/wizards" },
112
+ { text: "Kanban Boards", link: "/guides/kanban" },
91
113
  ]
92
114
  },
93
115
  {
@@ -177,6 +199,28 @@ export default defineConfig(withMermaid({
177
199
  { text: "Invites", link: "/reference/tenancy/invites" },
178
200
  ]
179
201
  },
202
+ {
203
+ text: "Wizard",
204
+ collapsed: false,
205
+ items: [
206
+ { text: "Overview", link: "/reference/wizard/" },
207
+ { text: "DSL", link: "/reference/wizard/dsl" },
208
+ { text: "Anchoring & resume", link: "/reference/wizard/anchoring-resume" },
209
+ { text: "Storage & config", link: "/reference/wizard/storage-config" },
210
+ { text: "Registration & launch", link: "/reference/wizard/registration-launch" },
211
+ { text: "One-time", link: "/reference/wizard/one-time" },
212
+ ]
213
+ },
214
+ {
215
+ text: "Kanban",
216
+ collapsed: false,
217
+ items: [
218
+ { text: "Overview", link: "/reference/kanban/" },
219
+ { text: "DSL", link: "/reference/kanban/dsl" },
220
+ { text: "Positioning", link: "/reference/kanban/positioning" },
221
+ { text: "Authorization", link: "/reference/kanban/authorization" },
222
+ ]
223
+ },
180
224
  {
181
225
  text: "Testing",
182
226
  collapsed: false,
@@ -0,0 +1,45 @@
1
+ // Syncs .claude/skills/*/SKILL.md into docs/public/skills/ so the docs site
2
+ // serves them as raw, crawlable markdown at /skills/<name>.md.
3
+ // Runs before `docs:dev` and `docs:build` (see package.json). Output is gitignored.
4
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"
5
+ import { join, dirname } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..")
9
+ const skillsDir = join(root, ".claude", "skills")
10
+ const outDir = join(root, "docs", "public", "skills")
11
+
12
+ rmSync(outDir, { recursive: true, force: true })
13
+ mkdirSync(outDir, { recursive: true })
14
+
15
+ const skills = []
16
+
17
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
18
+ if (!entry.isDirectory()) continue
19
+ const source = join(skillsDir, entry.name, "SKILL.md")
20
+ if (!existsSync(source)) continue // skip dirs that aren't skills (no SKILL.md)
21
+ let content = readFileSync(source, "utf8")
22
+
23
+ const description = content.match(/^description:\s*(.+)$/m)?.[1] ?? ""
24
+
25
+ // Wiki-style [[skill-name]] cross-links become relative markdown links so
26
+ // crawlers can follow them between the published files.
27
+ content = content.replace(/\[\[([\w-]+)\]\]/g, "[$1]($1.md)")
28
+
29
+ writeFileSync(join(outDir, `${entry.name}.md`), content)
30
+ skills.push({ name: entry.name, description })
31
+ }
32
+
33
+ skills.sort((a, b) => (a.name === "plutonium" ? -1 : b.name === "plutonium" ? 1 : a.name.localeCompare(b.name)))
34
+
35
+ const index = `# Plutonium Skills
36
+
37
+ Task-focused guides for AI agents working with the [Plutonium](https://radioactive-labs.github.io/plutonium-core/) Rails RAD framework. Each file is self-contained markdown. Start with \`plutonium.md\` — it routes to the others.
38
+
39
+ These are the same skills the gem installs into projects via \`rails g pu:skills:sync\` (Claude Code loads them automatically from \`.claude/skills/\`). Any agent can fetch them directly from the URLs below.
40
+
41
+ ${skills.map((s) => `- [${s.name}](${s.name}.md) — ${s.description}`).join("\n")}
42
+ `
43
+
44
+ writeFileSync(join(outDir, "index.md"), index)
45
+ console.log(`Synced ${skills.length} skills to docs/public/skills/`)
data/docs/ai.md ADDED
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: Plutonium for AI Agents
3
+ description: Machine-readable entry point for AI coding agents building Rails apps with Plutonium — install commands, conventions, generator catalog, and skill files.
4
+ sidebar: false
5
+ ---
6
+
7
+ # Plutonium for AI Agents
8
+
9
+ You are reading the agent entry point for **Plutonium**, a Rails RAD framework (Ruby gem) that provides resource-oriented architecture: automatic CRUD, authorization policies, declarative UI definitions, authentication, and multi-tenancy. If a user asked you to build something with Plutonium, this page gives you the golden path and where to read next.
10
+
11
+ ## Machine-readable resources
12
+
13
+ | Resource | URL |
14
+ |---|---|
15
+ | Docs index (llms.txt) | `https://radioactive-labs.github.io/plutonium-core/llms.txt` |
16
+ | Full docs, one file | `https://radioactive-labs.github.io/plutonium-core/llms-full.txt` |
17
+ | Agent skills index | `https://radioactive-labs.github.io/plutonium-core/skills/index.md` |
18
+ | Router skill (start here) | `https://radioactive-labs.github.io/plutonium-core/skills/plutonium.md` |
19
+
20
+ Every docs page also has a raw markdown twin: append `.md` to its URL (e.g. `/guides/multi-tenancy.md`).
21
+
22
+ **Prefer the skills over the raw docs.** They are task-focused, deduplicated, and encode the mistakes agents actually make. The router skill maps "about to do X" to the right skill file. If you are Claude Code, run `rails g pu:skills:sync` after installing the gem to install them into `.claude/skills/` so they load automatically.
23
+
24
+ ## Rules that prevent expensive mistakes
25
+
26
+ 1. **Plutonium is generator-driven.** Nearly every file has a `pu:*` generator. Generate, then edit — never hand-write models, definitions, policies, or packages from scratch. Hand-written files drift from conventions and break future generator runs.
27
+ 2. **Inspect before you act.** Check `Gemfile` for `plutonium`, `ls config/packages.rb`, and `ls packages/` before installing or scaffolding anything.
28
+ 3. **New app → `plutonium.rb` template. Existing app → `base.rb` template.** Never run `plutonium.rb` on an existing app; it re-runs full bootstrap and clobbers git history.
29
+ 4. **Pass flags to avoid interactive prompts**: `--dest=main_app` (or `--dest=<package>`), `--force` when re-running meta-generators, `--auth=<account>` for portals, `--skip-bundle`, `--quiet`.
30
+ 5. **Quote field arguments with special characters**: `'title:string?'`, `'price:decimal{10,2}'`.
31
+ 6. **Multi-tenancy is structural, not filtered in policies.** The portal declares `scope_to_entity`; the model provides the association path that `associated_with` resolves. Never `where(organization: ...)` in a policy — read the tenancy skill first.
32
+
33
+ ## Golden path: new application
34
+
35
+ ```bash
36
+ rails new myapp -a propshaft -j esbuild -c tailwind \
37
+ -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
38
+ cd myapp
39
+ rails db:prepare
40
+ ```
41
+
42
+ Then for each resource:
43
+
44
+ ```bash
45
+ rails g pu:res:scaffold Post 'title:string' 'body:text?' user:references --dest=main_app
46
+ rails db:prepare
47
+ rails g pu:res:conn Post --dest=<portal_package>
48
+ ```
49
+
50
+ ## Golden path: existing application
51
+
52
+ ```bash
53
+ bin/rails app:template \
54
+ LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
55
+ ```
56
+
57
+ Or manually: add `gem "plutonium"` to the Gemfile, `bundle install`, then `rails g pu:core:install`.
58
+
59
+ ## Architecture in one table
60
+
61
+ A **resource** is four cooperating layers. Plutonium auto-fills defaults from the model; you only declare overrides.
62
+
63
+ | Layer | File | Purpose |
64
+ |---|---|---|
65
+ | Model | `app/models/post.rb` | Data, validations, associations |
66
+ | Definition | `app/definitions/post_definition.rb` | UI — fields, filters, actions |
67
+ | Policy | `app/policies/post_policy.rb` | Authorization |
68
+ | Controller | `app/controllers/posts_controller.rb` | Rarely edited — use hooks |
69
+ | Interaction (optional) | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions |
70
+
71
+ Resources live in the main app or in **feature packages**; users access them through **portal packages** (Rails engines with their own auth and optional tenant scoping).
72
+
73
+ ## Generator catalog
74
+
75
+ Discover all generators with `rails g --help | grep pu:`. The most used:
76
+
77
+ | Generator | Purpose |
78
+ |---|---|
79
+ | `pu:core:install` | Initial Plutonium setup |
80
+ | `pu:res:scaffold NAME field:type ... --dest=` | New resource (model, migration, policy, definition) |
81
+ | `pu:res:conn RESOURCE --dest=PORTAL` | Connect a resource to a portal |
82
+ | `pu:pkg:package NAME` | Feature package |
83
+ | `pu:pkg:portal NAME --auth=...` | Portal package |
84
+ | `pu:rodauth:install` / `pu:rodauth:account NAME` | Authentication |
85
+ | `pu:saas:setup --user ... --entity ...` | Full SaaS bootstrap: user + tenant + membership + portal |
86
+ | `pu:test:install` / `pu:test:scaffold NAME` | Testing scaffolds |
87
+ | `pu:skills:sync` | Install the agent skill files into the project |
88
+
89
+ ## Verify your work
90
+
91
+ ```bash
92
+ rails runner "puts Plutonium::VERSION" # gem installed and loaded
93
+ rails db:prepare # migrations applied
94
+ bin/dev # boot; visit the portal route
95
+ ```
96
+
97
+ ## For humans reading this
98
+
99
+ This page exists so AI coding agents can bootstrap correctly on the first try. Point your agent at it, or just say: *"Read https://radioactive-labs.github.io/plutonium-core/ai.md before starting."* The [Getting Started guide](/getting-started/) covers the same ground for people.
data/docs/guides/index.md CHANGED
@@ -25,6 +25,8 @@ aside: false
25
25
  { name: 'Nested resources', link: '/plutonium-core/guides/nested-resources' },
26
26
  { name: 'Multi-tenancy', link: '/plutonium-core/guides/multi-tenancy' },
27
27
  { name: 'Search & filtering', link: '/plutonium-core/guides/search-filtering' },
28
+ { name: 'Wizards', desc: 'Multi-step flows — onboarding, checkout, branching create.', link: '/plutonium-core/guides/wizards' },
29
+ { name: 'Kanban boards', desc: 'Drag-and-drop board view — columns, moves, positioning, WIP limits.', link: '/plutonium-core/guides/kanban' },
28
30
  ]},
29
31
  { group: 'Customization', items: [
30
32
  { name: 'Customizing the UI', desc: 'A map of the override surface — pages, forms, displays, tables, components, layouts.', link: '/plutonium-core/guides/customizing-ui' },