plutonium 0.45.2 → 0.46.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +146 -0
  3. data/.claude/skills/plutonium-assets/SKILL.md +248 -157
  4. data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
  5. data/.claude/skills/plutonium-controller/SKILL.md +9 -2
  6. data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
  7. data/.claude/skills/plutonium-definition/SKILL.md +521 -7
  8. data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
  9. data/.claude/skills/plutonium-forms/SKILL.md +8 -1
  10. data/.claude/skills/plutonium-installation/SKILL.md +25 -2
  11. data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
  12. data/.claude/skills/plutonium-invites/SKILL.md +11 -7
  13. data/.claude/skills/plutonium-model/SKILL.md +50 -50
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
  15. data/.claude/skills/plutonium-package/SKILL.md +8 -1
  16. data/.claude/skills/plutonium-policy/SKILL.md +69 -78
  17. data/.claude/skills/plutonium-portal/SKILL.md +26 -70
  18. data/.claude/skills/plutonium-views/SKILL.md +9 -2
  19. data/CHANGELOG.md +33 -0
  20. data/app/assets/plutonium.css +1 -1
  21. data/app/views/rodauth/_login_form.html.erb +0 -3
  22. data/app/views/rodauth/confirm_password.html.erb +0 -4
  23. data/app/views/rodauth/create_account.html.erb +0 -3
  24. data/app/views/rodauth/logout.html.erb +0 -3
  25. data/config/initializers/pagy.rb +1 -1
  26. data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
  27. data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
  28. data/gemfiles/rails_7.gemfile.lock +1 -1
  29. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  31. data/lib/generators/pu/core/update/update_generator.rb +8 -0
  32. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
  33. data/lib/generators/pu/invites/install_generator.rb +8 -1
  34. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
  35. data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
  36. data/lib/generators/pu/profile/conn_generator.rb +9 -12
  37. data/lib/generators/pu/profile/install_generator.rb +5 -2
  38. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  39. data/lib/generators/pu/saas/portal_generator.rb +4 -9
  40. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
  41. data/lib/plutonium/engine.rb +18 -5
  42. data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
  43. data/lib/plutonium/version.rb +1 -1
  44. data/package.json +1 -1
  45. metadata +7 -8
  46. data/.claude/skills/plutonium/skill.md +0 -130
  47. data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
  48. data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
  49. data/.claude/skills/plutonium-profile/SKILL.md +0 -276
  50. data/.claude/skills/plutonium-theming/SKILL.md +0 -424
@@ -1,10 +1,31 @@
1
1
  ---
2
2
  name: plutonium-policy
3
- description: Use when configuring authorization - action permissions, attribute visibility, relation scoping, or per-portal policies
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
4
  ---
5
5
 
6
6
  # Plutonium Policies
7
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
+
8
29
  **Policies are generated automatically** - never create them manually:
9
30
  - `rails g pu:res:scaffold` creates the base policy
10
31
  - `rails g pu:res:conn` creates portal-specific policies with attribute permissions
@@ -153,119 +174,89 @@ def permitted_attributes_for_read
153
174
  end
154
175
  ```
155
176
 
156
- ### Auto-Detection (Development Only)
157
-
158
- In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
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`.
159
178
 
160
- ## Association Permissions
161
-
162
- Control which associations can be rendered:
179
+ ### Anti-pattern: nested_attributes hashes in permitted_attributes
163
180
 
164
181
  ```ruby
165
- def permitted_associations
166
- %i[comments tags author]
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
+ ]
167
189
  end
168
190
  ```
169
191
 
170
- Used for:
171
- - Nested forms
172
- - Related data displays
173
- - Association fields in tables
174
-
175
- ## Collection Scoping
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=>[...]}]`.
176
193
 
177
- Filter which records users can see:
194
+ The correct pattern:
178
195
 
179
196
  ```ruby
180
- relation_scope do |relation|
181
- if user.admin?
182
- relation
183
- else
184
- relation.where(author: user)
185
- end
197
+ # Policy permits just the association name
198
+ def permitted_attributes_for_create
199
+ [:name, :variants, :comments]
186
200
  end
187
201
  ```
188
202
 
189
- ### With Parent Scoping (Nested Resources)
190
-
191
- For nested resources, call `super` to apply automatic parent scoping:
192
-
193
203
  ```ruby
194
- relation_scope do |relation|
195
- relation = super(relation) # Applies parent scoping automatically
196
-
197
- if user.admin?
198
- relation
199
- else
200
- relation.where(published: true)
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
201
209
  end
202
210
  end
203
211
  ```
204
212
 
205
- **Parent scoping takes precedence over entity scoping.** When a parent is present:
206
- - For `has_many`: scopes via `parent.association_name`
207
- - For `has_one`: scopes via `where(foreign_key: parent.id)`
208
-
209
- ### With Entity Scoping (Multi-tenancy)
210
-
211
- When no parent is present, `super` applies entity scoping:
212
-
213
213
  ```ruby
214
- relation_scope do |relation|
215
- relation = super(relation) # Apply entity scope if no parent
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
216
219
 
217
- if user.admin?
218
- relation
219
- else
220
- relation.where(published: true)
221
- end
220
+ class Variant < ApplicationRecord
221
+ belongs_to :post, inverse_of: :variants # ← required for nested validation
222
222
  end
223
223
  ```
224
224
 
225
- ### default_relation_scope is Required
226
-
227
- Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
225
+ See `plutonium-definition` for the full `nested_input` API.
228
226
 
229
- ```ruby
230
- # ❌ This will raise an error
231
- relation_scope do |relation|
232
- relation.where(published: true) # Missing default_relation_scope!
233
- end
227
+ ### Auto-Detection (Development Only)
234
228
 
235
- # Correct - call default_relation_scope
236
- relation_scope do |relation|
237
- default_relation_scope(relation).where(published: true)
238
- end
229
+ In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
239
230
 
240
- # Also correct - super calls default_relation_scope
241
- relation_scope do |relation|
242
- super(relation).where(published: true)
243
- end
244
- ```
231
+ ## Association Permissions
245
232
 
246
- When overriding an inherited scope:
233
+ Control which associations can be rendered:
247
234
 
248
235
  ```ruby
249
- class AdminPostPolicy < PostPolicy
250
- relation_scope do |relation|
251
- # Replace inherited scope but keep Plutonium's parent/entity scoping
252
- default_relation_scope(relation)
253
- end
236
+ def permitted_associations
237
+ %i[comments tags author]
254
238
  end
255
239
  ```
256
240
 
257
- ### Skipping Default Scoping (Rare)
241
+ Used for:
242
+ - Nested forms
243
+ - Related data displays
244
+ - Association fields in tables
258
245
 
259
- If you intentionally need to skip scoping, call `skip_default_relation_scope!`:
246
+ ## Collection Scoping (relation_scope)
247
+
248
+ Filter which records users can see:
260
249
 
261
250
  ```ruby
262
251
  relation_scope do |relation|
263
- skip_default_relation_scope!
264
- relation # No parent/entity scoping applied
252
+ relation = default_relation_scope(relation)
253
+ user.admin? ? relation : relation.where(author: user)
265
254
  end
266
255
  ```
267
256
 
268
- Consider using a separate portal instead of skipping scoping.
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.**
269
260
 
270
261
  ## Portal-Specific Policies
271
262
 
@@ -453,7 +444,7 @@ end
453
444
 
454
445
  1. **Always override `create?` and `read?`** - They default to `false`
455
446
  2. **Define attributes explicitly** - Auto-detection only works in development
456
- 3. **Call `super` in `relation_scope`** - Preserves entity scoping
447
+ 3. **Call `default_relation_scope(relation)` in `relation_scope`** - Preserves parent/entity scoping (do not rely on `super` from inside the block)
457
448
  4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
458
449
  5. **Keep policies focused** - Authorization logic only, no business logic
459
450
  6. **Test edge cases** - Archived records, nil associations, role combinations
@@ -461,5 +452,5 @@ end
461
452
  ## Related Skills
462
453
 
463
454
  - `plutonium` - How policies fit in the resource architecture
464
- - `plutonium-definition-actions` - Actions that need policy methods
455
+ - `plutonium-definition` - Actions that need policy methods
465
456
  - `plutonium-controller` - How controllers use policies
@@ -1,12 +1,32 @@
1
1
  ---
2
2
  name: plutonium-portal
3
- description: Use when creating portals, connecting resources to portals, configuring authentication, entity scoping, or portal routes
3
+ description: Use BEFORE creating a portal, mounting a portal engine, running pu:pkg:portal, configuring entity strategies, or routing portal-specific resources. For tenancy mechanics, also load plutonium-entity-scoping.
4
4
  ---
5
5
 
6
6
  # Plutonium Portals
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Use `pu:pkg:portal`.** Never hand-craft a portal engine — the generator wires up the controller concerns, routes, and layout.
10
+ - **Always use `pu:res:conn` to connect resources to portals** — resources are invisible until connected. Pass resources directly (not via `--src`) to skip prompts.
11
+ - **Entity scoping is portal-level.** `scope_to_entity Entity, strategy: :path` in the portal engine; then every resource in that portal is scoped automatically. For mechanics, see `plutonium-entity-scoping`.
12
+ - **Pass `--auth=<account>` / `--public` / `--byo`** to `pu:pkg:portal` for unattended runs.
13
+ - **Related skills:** `plutonium-entity-scoping` (tenancy mechanics), `plutonium-auth` (Rodauth integration), `plutonium-package` (portal vs feature packages), `plutonium-policy` (portal-specific policies).
14
+
8
15
  Portals are Rails engines that provide web interfaces for specific user types.
9
16
 
17
+ ## Quick checklist
18
+
19
+ Creating a portal and connecting resources:
20
+
21
+ 1. Run `rails g pu:pkg:portal <name> --auth=<account>` (or `--public` / `--byo`). Add `--scope=Entity` for multi-tenancy.
22
+ 2. Mount the engine in `config/routes.rb`: `mount <Name>Portal::Engine, at: "/<name>"`.
23
+ 3. For each resource, run `rails g pu:res:conn ResourceName --dest=<name>_portal`.
24
+ 4. For singular resources (profile, settings), pass `--singular`.
25
+ 5. Customize the portal's `Concerns::Controller` for auth / before_action hooks.
26
+ 6. Override portal-specific policies/definitions as needed.
27
+ 7. Verify: `bin/rails routes | grep <name>_portal`.
28
+ 8. For multi-tenancy specifics, load `plutonium-entity-scoping`.
29
+
10
30
  ## Creating a Portal
11
31
 
12
32
  ```bash
@@ -181,11 +201,7 @@ end
181
201
 
182
202
  ## Entity Scoping (Multi-tenancy)
183
203
 
184
- Automatically scope all data to a parent entity.
185
-
186
- ### Path Strategy
187
-
188
- Entity ID in URL path:
204
+ Portals can scope all data to a parent entity via `scope_to_entity`:
189
205
 
190
206
  ```ruby
191
207
  module AdminPortal
@@ -199,71 +215,11 @@ module AdminPortal
199
215
  end
200
216
  ```
201
217
 
202
- Routes become: `/organizations/:organization_id/posts`
203
-
204
- ### Custom Strategy
205
-
206
- Implement your own lookup method:
207
-
208
- ```ruby
209
- module AdminPortal
210
- class Engine < Rails::Engine
211
- include Plutonium::Portal::Engine
218
+ Strategies: `:path` (entity id in URL) or a custom method name on the portal controller concern.
212
219
 
213
- config.after_initialize do
214
- scope_to_entity Organization, strategy: :current_organization
215
- end
216
- end
217
- end
220
+ Access in controllers/views: `current_scoped_entity`, `scoped_to_entity?`. In policies: `entity_scope`.
218
221
 
219
- # In controller concern
220
- module AdminPortal
221
- module Concerns
222
- module Controller
223
- extend ActiveSupport::Concern
224
- include Plutonium::Portal::Controller
225
-
226
- private
227
-
228
- # Method name must match strategy
229
- def current_organization
230
- @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
231
- end
232
- end
233
- end
234
- end
235
- ```
236
-
237
- ### Accessing the Scoped Entity
238
-
239
- ```ruby
240
- current_scoped_entity # The current Organization/Account/etc.
241
- scoped_to_entity? # true if scoping is active
242
- ```
243
-
244
- ### Model Requirements
245
-
246
- Models must have an association path to the scoped entity:
247
-
248
- ```ruby
249
- # Direct association (preferred)
250
- class Post < ResourceRecord
251
- belongs_to :organization
252
- end
253
-
254
- # Through association
255
- class Comment < ResourceRecord
256
- belongs_to :post
257
- has_one :organization, through: :post
258
- end
259
-
260
- # Complex (define custom scope)
261
- class AuditLog < ResourceRecord
262
- scope :associated_with_organization, ->(org) {
263
- joins(:user).where(users: { organization_id: org.id })
264
- }
265
- end
266
- ```
222
+ > **For the full entity scoping picture — the three model shapes, `associated_with` resolution, `default_relation_scope` rules, safe `relation_scope` overrides, and how parent scoping takes precedence — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
267
223
 
268
224
  ## Routes
269
225
 
@@ -448,7 +404,7 @@ rails g pu:res:conn Post --dest=admin_portal
448
404
  ## Related Skills
449
405
 
450
406
  - `plutonium-package` - Package overview (features vs portals)
451
- - `plutonium-rodauth` - Authentication setup and configuration
407
+ - `plutonium-auth` - Authentication setup and configuration
452
408
  - `plutonium-policy` - Portal-specific policies
453
409
  - `plutonium-definition` - Portal-specific definitions
454
410
  - `plutonium-controller` - Portal-specific controllers
@@ -1,10 +1,17 @@
1
1
  ---
2
2
  name: plutonium-views
3
- description: Use when building custom pages, display panels, tables, or layouts using Phlex components in Plutonium
3
+ description: Use BEFORE building a custom page, panel, table, layout, or Phlex component in Plutonium. Also when overriding IndexPage/ShowPage/Form classes in a definition.
4
4
  ---
5
5
 
6
6
  # Plutonium Views
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end` — don't replace the entire view layer.
10
+ - **Use the render hooks.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. — they exist so you don't have to override `view_template` and re-implement everything.
11
+ - **All pages inherit `DynaFrameContent`** so turbo-frame requests render only the content. Don't fight it — modals and frame nav "just work".
12
+ - **For custom components, use `Plutonium::UI::Component::Base`** so you inherit the component kit (`PageHeader`, `Panel`, `Block`, etc.) and access to resource helpers.
13
+ - **Related skills:** `plutonium-forms` (form customization), `plutonium-assets` (theming + component classes), `plutonium-definition` (field-level rendering), `plutonium-controller` (presentation hooks like `present_parent?`).
14
+
8
15
  Plutonium uses [Phlex](https://www.phlex.fun/) for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
9
16
 
10
17
  ## Architecture Overview
@@ -580,6 +587,6 @@ end
580
587
  - `plutonium-forms` - Custom form templates and field builders
581
588
  - `plutonium-assets` - TailwindCSS and component theming
582
589
  - `plutonium-definition` - Field/input/display configuration
583
- - `plutonium-definition-actions` - Action buttons and interactions
590
+ - `plutonium-definition` - Action buttons and interactions
584
591
  - `plutonium-controller` - Presentation hooks (`present_parent?`, etc.)
585
592
  - `plutonium-portal` - Portal-specific customization
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## [0.46.0] - 2026-04-11
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(profile)* Default profile model to {UserModel}Profile
6
+ - *(generators)* Sync skills during pu:core:update if plutonium skill is installed
7
+ - *(generators/active_shrine)* Disable Active Storage railtie and include ActiveShrine::Model
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - *(engine)* Resolve scoped entity class lazily to survive autoreload
12
+ - *(rodauth)* Render page title in layout, drop per-view h1s
13
+ - *(generators/invites)* Use derived user association in current_membership
14
+ - *(generators/rodauth)* Redirect to login after verification email sent
15
+
16
+ ### 🚜 Refactor
17
+
18
+ - *(generators)* Add inject_into_concerns_controller to merge included blocks
19
+
20
+ ### 📚 Documentation
21
+
22
+ - *(skills)* Clarify generator gotchas for installation, rodauth, and unattended runs
23
+ - *(skills)* Comprehensive Plutonium skills overhaul
24
+ - *(skills)* Document nested_attributes gotchas in policy and definition
25
+
26
+ ### ⚙️ Miscellaneous Tasks
27
+
28
+ - Update test lockfiles
29
+ ## [0.45.3] - 2026-04-07
30
+
31
+ ### 🐛 Bug Fixes
32
+
33
+ - *(pagy)* Use Pagy::OPTIONS instead of frozen DEFAULT
1
34
  ## [0.45.2] - 2026-04-07
2
35
 
3
36
  ### 🐛 Bug Fixes