plutonium 0.50.0 → 0.51.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -1,456 +0,0 @@
1
- ---
2
- name: plutonium-policy
3
- description: Use BEFORE writing relation_scope, permitted_attributes, permitted_associations, or any policy override. For tenant-scoped relation_scope, also load plutonium-entity-scoping.
4
- ---
5
-
6
- # Plutonium Policies
7
-
8
- ## 🚨 Critical (read first)
9
- - **Use generators.** `pu:res:scaffold` and `pu:res:conn` create policies — never hand-write policy files.
10
- - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with a raw `where(organization: …)` or manual joins skips entity scoping and triggers `verify_default_relation_scope_applied!` at runtime. Compose like this: `relation_scope { |r| default_relation_scope(r).where(archived: false) }`. Call `default_relation_scope(r)` **explicitly** — `super` is unreliable inside the DSL block. Full rules in `plutonium-entity-scoping`.
11
- - **Derived actions inherit.** `update?` falls back to `create?`, `show?` falls back to `read?` — don't duplicate unless the rules genuinely differ. Override `create?` and `read?` explicitly; they default to `false`.
12
- - **Define `permitted_attributes_for_*` explicitly.** Auto-detection works in development but raises in production.
13
- - **For `has_cents` fields, list the virtual name (`:price`), not the column (`:price_cents`).** Generators occasionally emit the wrong one — fix it (and verify the model has `has_cents`). See `plutonium-model` › Monetary Handling.
14
- - **Related skills:** `plutonium-entity-scoping` (tenant-scoped overrides — required for `relation_scope`), `plutonium-model` (`associated_with`), `plutonium-definition` (`permitted_attributes` usage), `plutonium-controller` (how controllers use policies).
15
-
16
- ## Quick checklist
17
-
18
- Writing / editing a policy:
19
-
20
- 1. Confirm the policy was created by `pu:res:scaffold` or `pu:res:conn`.
21
- 2. Override `create?` and `read?` explicitly — they default to `false`.
22
- 3. Define `permitted_attributes_for_read` and `permitted_attributes_for_create` (derived methods inherit).
23
- 4. For custom actions, add `def <action>?` matching the definition's `action :<action>`.
24
- 5. If you need `relation_scope`, compose with `default_relation_scope(relation).where(...)` — never bypass it.
25
- 6. For tenant scoping, load `plutonium-entity-scoping` and fix the **model**, not the policy.
26
- 7. Per-portal overrides go in the portal's policy file (created by `pu:res:conn`).
27
- 8. Test: log in as a user who should NOT see a record, verify it's filtered out.
28
-
29
- **Policies are generated automatically** - never create them manually:
30
- - `rails g pu:res:scaffold` creates the base policy
31
- - `rails g pu:res:conn` creates portal-specific policies with attribute permissions
32
-
33
- Policies control WHO can do WHAT with resources. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/).
34
-
35
- Plutonium extends ActionPolicy with:
36
- - Attribute permissions (`permitted_attributes_for_*`)
37
- - Association permissions (`permitted_associations`)
38
- - Automatic entity scoping for multi-tenancy
39
- - Derived action methods (e.g., `update?` inherits from `create?`)
40
-
41
- ## Base Class
42
-
43
- ```ruby
44
- # app/policies/resource_policy.rb (generated during install)
45
- class ResourcePolicy < Plutonium::Resource::Policy
46
- # App-wide authorization defaults
47
- end
48
-
49
- # app/policies/post_policy.rb (per resource)
50
- class PostPolicy < ResourcePolicy
51
- def create?
52
- user.present?
53
- end
54
-
55
- def read?
56
- true
57
- end
58
-
59
- def permitted_attributes_for_create
60
- %i[title content]
61
- end
62
-
63
- def permitted_attributes_for_read
64
- %i[title content author created_at]
65
- end
66
- end
67
- ```
68
-
69
- ## Action Permissions
70
-
71
- ### Core Actions (Must Override)
72
-
73
- ```ruby
74
- def create? # Default: false - MUST override
75
- user.present?
76
- end
77
-
78
- def read? # Default: false - MUST override
79
- true
80
- end
81
- ```
82
-
83
- ### Derived Actions (Inherit by Default)
84
-
85
- | Method | Inherits From | Override When |
86
- |--------|---------------|---------------|
87
- | `update?` | `create?` | Different update rules |
88
- | `destroy?` | `create?` | Different delete rules |
89
- | `index?` | `read?` | Custom listing rules |
90
- | `show?` | `read?` | Record-specific read rules |
91
- | `new?` | `create?` | Rarely needed |
92
- | `edit?` | `update?` | Rarely needed |
93
- | `search?` | `index?` | Search-specific rules |
94
-
95
- ### Custom Actions
96
-
97
- Define methods matching your action names:
98
-
99
- ```ruby
100
- def publish?
101
- update? && record.draft?
102
- end
103
-
104
- def archive?
105
- create? && !record.archived?
106
- end
107
-
108
- def invite_user?
109
- user.admin?
110
- end
111
- ```
112
-
113
- Actions are secure by default - undefined methods return `false`.
114
-
115
- ### Bulk Action Authorization
116
-
117
- Bulk actions (operating on multiple selected records) support **per-record authorization**:
118
-
119
- ```ruby
120
- def bulk_archive?
121
- create? && !record.locked? # Per-record check
122
- end
123
-
124
- def bulk_publish?
125
- user.admin? || record.author == user
126
- end
127
- ```
128
-
129
- **How bulk authorization works:**
130
- 1. Policy method (e.g., `bulk_archive?`) is checked **per record** in the selection
131
- 2. **Backend:** If any selected record fails authorization, the entire request is rejected
132
- 3. **UI:** Only actions that **all** selected records support are shown (intersection)
133
- 4. Records are fetched via `current_authorized_scope` - only accessible records can be selected
134
-
135
- This provides full per-record authorization while keeping the UI clean - users only see actions they can actually perform on their entire selection.
136
-
137
- ## Attribute Permissions
138
-
139
- ### Core Methods (Must Override for Production)
140
-
141
- ```ruby
142
- # What users can see (index, show)
143
- def permitted_attributes_for_read
144
- %i[title content author published_at created_at]
145
- end
146
-
147
- # What users can set (create, update)
148
- def permitted_attributes_for_create
149
- %i[title content]
150
- end
151
- ```
152
-
153
- ### Derived Methods (Inherit by Default)
154
-
155
- | Method | Inherits From |
156
- |--------|---------------|
157
- | `permitted_attributes_for_update` | `permitted_attributes_for_create` |
158
- | `permitted_attributes_for_index` | `permitted_attributes_for_read` |
159
- | `permitted_attributes_for_show` | `permitted_attributes_for_read` |
160
- | `permitted_attributes_for_new` | `permitted_attributes_for_create` |
161
- | `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
162
-
163
- ### Per-Action Attributes
164
-
165
- Show different fields for different views:
166
-
167
- ```ruby
168
- def permitted_attributes_for_index
169
- %i[title author created_at] # Minimal for list
170
- end
171
-
172
- def permitted_attributes_for_read
173
- %i[title content author tags created_at updated_at] # Full for detail
174
- end
175
- ```
176
-
177
- **Key insight:** `permitted_attributes_for_*` controls *which fields appear* on each view (form, show, index). The `column`/`field`/`input`/`display` declarations in the **definition** only control *how those fields render* — they do NOT add or remove fields from the page. If your index page is showing fields you didn't want, override `permitted_attributes_for_index` (it does NOT inherit from `_for_read` automatically when you want a different shape). The same applies to forms: a `field :name` in the definition won't be rendered unless `:name` is in `permitted_attributes_for_create`/`_update`.
178
-
179
- ### Anti-pattern: nested_attributes hashes in permitted_attributes
180
-
181
- ```ruby
182
- # ❌ DO NOT DO THIS
183
- def permitted_attributes_for_create
184
- [
185
- :name,
186
- {variants_attributes: [:id, :name, :_destroy]},
187
- {comments_attributes: [:id, :body, :_destroy]}
188
- ]
189
- end
190
- ```
191
-
192
- Plutonium's form pipeline extracts nested params via the **form definition** (`build_form(...).extract_input(...)`), not the policy. Hash entries in `permitted_attributes_for_*` get iterated as field names by the form renderer and end up as literal text inputs with names like `model[{:variants_attributes=>[...]}]`.
193
-
194
- The correct pattern:
195
-
196
- ```ruby
197
- # ✅ Policy permits just the association name
198
- def permitted_attributes_for_create
199
- [:name, :variants, :comments]
200
- end
201
- ```
202
-
203
- ```ruby
204
- # ✅ Definition declares the nested input (this drives both rendering AND param extraction)
205
- class PostDefinition < ResourceDefinition
206
- nested_input :variants do |n|
207
- n.input :name
208
- n.input :is_default, as: :boolean
209
- end
210
- end
211
- ```
212
-
213
- ```ruby
214
- # ✅ Model declares accepts_nested_attributes_for + inverse_of on the back-reference
215
- class Post < ApplicationRecord
216
- has_many :variants, inverse_of: :post, dependent: :destroy
217
- accepts_nested_attributes_for :variants, allow_destroy: true, reject_if: :all_blank
218
- end
219
-
220
- class Variant < ApplicationRecord
221
- belongs_to :post, inverse_of: :variants # ← required for nested validation
222
- end
223
- ```
224
-
225
- See `plutonium-definition` for the full `nested_input` API.
226
-
227
- ### Auto-Detection (Development Only)
228
-
229
- In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
230
-
231
- ## Association Permissions
232
-
233
- Control which associations can be rendered:
234
-
235
- ```ruby
236
- def permitted_associations
237
- %i[comments tags author]
238
- end
239
- ```
240
-
241
- Used for:
242
- - Nested forms
243
- - Related data displays
244
- - Association fields in tables
245
-
246
- ## Collection Scoping (relation_scope)
247
-
248
- Filter which records users can see:
249
-
250
- ```ruby
251
- relation_scope do |relation|
252
- relation = default_relation_scope(relation)
253
- user.admin? ? relation : relation.where(author: user)
254
- end
255
- ```
256
-
257
- **Always compose with `default_relation_scope(relation)` explicitly** — not `super`. Plutonium enforces this via `verify_default_relation_scope_applied!`. Anything else (a raw `where(organization: ...)`, manual joins) bypasses Plutonium's tenancy handling and will raise.
258
-
259
- > **For the full rules — why `default_relation_scope` is required, how parent vs entity scoping interact, safe override patterns, `skip_default_relation_scope!`, and how `associated_with` resolution works — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth for Plutonium tenant scoping.**
260
-
261
- ## Portal-Specific Policies
262
-
263
- Override policies per portal:
264
-
265
- ```ruby
266
- # Base policy
267
- class PostPolicy < ResourcePolicy
268
- def create?
269
- user.present?
270
- end
271
- end
272
-
273
- # Admin portal - more permissive
274
- class AdminPortal::PostPolicy < ::PostPolicy
275
- include AdminPortal::ResourcePolicy
276
-
277
- def destroy?
278
- true # Admins can always delete
279
- end
280
-
281
- def permitted_attributes_for_create
282
- %i[title content featured internal_notes] # More fields
283
- end
284
- end
285
-
286
- # Public portal - restricted
287
- class PublicPortal::PostPolicy < ::PostPolicy
288
- include PublicPortal::ResourcePolicy
289
-
290
- def create?
291
- false # No public creation
292
- end
293
- end
294
- ```
295
-
296
- ## Common Patterns
297
-
298
- ### Check Model Capabilities
299
-
300
- ```ruby
301
- def archive?
302
- return false unless record.respond_to?(:archived!)
303
- return false if record.archived?
304
-
305
- user.admin?
306
- end
307
- ```
308
-
309
- ### Prevent Actions on Archived Records
310
-
311
- ```ruby
312
- def update?
313
- return false if record.try(:archived?)
314
- super
315
- end
316
-
317
- def destroy?
318
- return false if record.try(:archived?)
319
- super
320
- end
321
- ```
322
-
323
- ### Owner-Based Permissions
324
-
325
- ```ruby
326
- def update?
327
- record.author == user || user.admin?
328
- end
329
-
330
- def destroy?
331
- update? # Same rules as update
332
- end
333
- ```
334
-
335
- ### Role-Based Permissions
336
-
337
- ```ruby
338
- def create?
339
- user.admin? || user.editor?
340
- end
341
-
342
- def read?
343
- true # Everyone can read
344
- end
345
-
346
- def update?
347
- return true if user.admin?
348
- return true if user.editor? && record.author == user
349
- false
350
- end
351
- ```
352
-
353
- ### Conditional Attribute Access
354
-
355
- ```ruby
356
- def permitted_attributes_for_create
357
- attrs = %i[title content]
358
- attrs << :featured if user.admin?
359
- attrs << :author_id if user.admin? # Only admins can set author
360
- attrs
361
- end
362
- ```
363
-
364
- ## Authorization Context
365
-
366
- Policies have access to:
367
-
368
- ```ruby
369
- user # Current user (required)
370
- record # The resource being authorized
371
- entity_scope # Current scoped entity (for multi-tenancy)
372
- parent # Parent record for nested resources (nil if not nested)
373
- parent_association # Association name on parent (e.g., :comments)
374
- ```
375
-
376
- ### Nested Resource Context
377
-
378
- For nested resources (e.g., `/posts/123/nested_comments`), the policy receives:
379
-
380
- ```ruby
381
- class CommentPolicy < ResourcePolicy
382
- def create?
383
- # parent is the Post instance
384
- # parent_association is :comments
385
- parent.present? && user.can_comment_on?(parent)
386
- end
387
-
388
- relation_scope do |relation|
389
- # super() uses parent and parent_association for scoping
390
- relation = super(relation)
391
- relation
392
- end
393
- end
394
- ```
395
-
396
- ### Custom Context
397
-
398
- Add custom context in controllers:
399
-
400
- ```ruby
401
- # In policy
402
- class PostPolicy < ResourcePolicy
403
- authorize :department, allow_nil: true
404
-
405
- def create?
406
- department&.allows_posting?
407
- end
408
- end
409
-
410
- # In controller
411
- class PostsController < ResourceController
412
- authorize :department, through: :current_department
413
-
414
- private
415
-
416
- def current_department
417
- current_user.department
418
- end
419
- end
420
- ```
421
-
422
- ## Controller Integration
423
-
424
- Built-in CRUD actions automatically:
425
- - Call `authorize_current!` at the start of each action
426
- - Apply `relation_scope` for index/listings
427
- - Filter params through `permitted_attributes`
428
-
429
- After-action callbacks verify authorization was performed - if you add custom actions, you must call `authorize_current!` yourself or skip verification.
430
-
431
- ### Skip Verification (When Needed)
432
-
433
- ```ruby
434
- class PostsController < ResourceController
435
- skip_verify_authorize_current only: [:custom_action]
436
-
437
- def custom_action
438
- # Handle authorization manually
439
- end
440
- end
441
- ```
442
-
443
- ## Best Practices
444
-
445
- 1. **Always override `create?` and `read?`** - They default to `false`
446
- 2. **Define attributes explicitly** - Auto-detection only works in development
447
- 3. **Call `default_relation_scope(relation)` in `relation_scope`** - Preserves parent/entity scoping (do not rely on `super` from inside the block)
448
- 4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
449
- 5. **Keep policies focused** - Authorization logic only, no business logic
450
- 6. **Test edge cases** - Archived records, nil associations, role combinations
451
-
452
- ## Related Skills
453
-
454
- - `plutonium` - How policies fit in the resource architecture
455
- - `plutonium-definition` - Actions that need policy methods
456
- - `plutonium-controller` - How controllers use policies