claude-agent-framework 1.0.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 (111) hide show
  1. package/README.md +128 -0
  2. package/bin/claude-framework +3 -0
  3. package/framework/agents/design-lead.md +240 -0
  4. package/framework/agents/product-owner.md +179 -0
  5. package/framework/agents/tech-lead.md +226 -0
  6. package/framework/commands/ayuda.md +127 -0
  7. package/framework/commands/a/303/261adir.md +98 -0
  8. package/framework/commands/backup.md +397 -0
  9. package/framework/commands/cambiar.md +110 -0
  10. package/framework/commands/cloud.md +457 -0
  11. package/framework/commands/code.md +142 -0
  12. package/framework/commands/debug.md +334 -0
  13. package/framework/commands/deploy.md +383 -0
  14. package/framework/commands/deshacer.md +120 -0
  15. package/framework/commands/estado.md +218 -0
  16. package/framework/commands/explica.md +227 -0
  17. package/framework/commands/feature.md +120 -0
  18. package/framework/commands/git.md +427 -0
  19. package/framework/commands/historial.md +202 -0
  20. package/framework/commands/learn.md +408 -0
  21. package/framework/commands/movil.md +245 -0
  22. package/framework/commands/nuevo.md +118 -0
  23. package/framework/commands/plan.md +134 -0
  24. package/framework/commands/prd.md +113 -0
  25. package/framework/commands/probar.md +148 -0
  26. package/framework/commands/revisar.md +208 -0
  27. package/framework/commands/seeds.md +230 -0
  28. package/framework/commands/seguridad.md +226 -0
  29. package/framework/commands/tasks.md +157 -0
  30. package/framework/skills/architecture/algorithms.md +970 -0
  31. package/framework/skills/architecture/clean-code.md +1080 -0
  32. package/framework/skills/architecture/design-patterns.md +1984 -0
  33. package/framework/skills/architecture/functional-programming.md +972 -0
  34. package/framework/skills/architecture/solid.md +991 -0
  35. package/framework/skills/cloud/cloud-aws.md +848 -0
  36. package/framework/skills/cloud/cloud-azure.md +931 -0
  37. package/framework/skills/cloud/cloud-gcp.md +848 -0
  38. package/framework/skills/cloud/message-queues.md +1229 -0
  39. package/framework/skills/core/accessibility.md +401 -0
  40. package/framework/skills/core/api.md +474 -0
  41. package/framework/skills/core/authentication.md +306 -0
  42. package/framework/skills/core/authorization.md +388 -0
  43. package/framework/skills/core/background-jobs.md +341 -0
  44. package/framework/skills/core/caching.md +473 -0
  45. package/framework/skills/core/code-review.md +341 -0
  46. package/framework/skills/core/controllers.md +290 -0
  47. package/framework/skills/core/cua.md +285 -0
  48. package/framework/skills/core/documentation.md +472 -0
  49. package/framework/skills/core/file-uploads.md +351 -0
  50. package/framework/skills/core/hotwire-native.md +296 -0
  51. package/framework/skills/core/hotwire.md +278 -0
  52. package/framework/skills/core/i18n.md +334 -0
  53. package/framework/skills/core/imports-exports.md +750 -0
  54. package/framework/skills/core/infrastructure.md +337 -0
  55. package/framework/skills/core/models.md +228 -0
  56. package/framework/skills/core/notifications.md +672 -0
  57. package/framework/skills/core/payments.md +581 -0
  58. package/framework/skills/core/performance.md +361 -0
  59. package/framework/skills/core/rails-scaffold.md +131 -0
  60. package/framework/skills/core/search.md +518 -0
  61. package/framework/skills/core/security.md +565 -0
  62. package/framework/skills/core/seeds.md +307 -0
  63. package/framework/skills/core/seo.md +542 -0
  64. package/framework/skills/core/testing.md +393 -0
  65. package/framework/skills/core/views.md +260 -0
  66. package/framework/skills/core/websockets.md +564 -0
  67. package/framework/skills/data/advanced-sql.md +1204 -0
  68. package/framework/skills/data/nosql.md +1141 -0
  69. package/framework/skills/devops/containers-advanced.md +1237 -0
  70. package/framework/skills/devops/debugging.md +834 -0
  71. package/framework/skills/devops/git-workflow.md +752 -0
  72. package/framework/skills/devops/networking.md +932 -0
  73. package/framework/skills/devops/shell-scripting.md +1132 -0
  74. package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
  75. package/framework/sub-agents/cloud-agent.md +677 -0
  76. package/framework/sub-agents/data.md +504 -0
  77. package/framework/sub-agents/debugging-agent.md +554 -0
  78. package/framework/sub-agents/devops.md +483 -0
  79. package/framework/sub-agents/docs.md +176 -0
  80. package/framework/sub-agents/frontend-dev.md +349 -0
  81. package/framework/sub-agents/git-workflow-agent.md +697 -0
  82. package/framework/sub-agents/integrations.md +630 -0
  83. package/framework/sub-agents/native-dev.md +434 -0
  84. package/framework/sub-agents/qa.md +138 -0
  85. package/framework/sub-agents/rails-dev.md +375 -0
  86. package/framework/sub-agents/security.md +526 -0
  87. package/framework/sub-agents/ui.md +437 -0
  88. package/framework/sub-agents/ux.md +284 -0
  89. package/framework/templates/api-spec.md +500 -0
  90. package/framework/templates/component-spec.md +248 -0
  91. package/framework/templates/feature.json +13 -0
  92. package/framework/templates/model-spec.md +318 -0
  93. package/framework/templates/prd-template.md +80 -0
  94. package/framework/templates/task-plan.md +122 -0
  95. package/framework/templates/task-user-story.md +52 -0
  96. package/framework/templates/technical-spec.md +260 -0
  97. package/framework/templates/user-story.md +95 -0
  98. package/package.json +42 -0
  99. package/project-templates/CLAUDE.md +42 -0
  100. package/project-templates/contexts/architecture.md +25 -0
  101. package/project-templates/contexts/conventions.md +46 -0
  102. package/project-templates/contexts/design-system.md +47 -0
  103. package/project-templates/contexts/requirements.md +38 -0
  104. package/project-templates/contexts/stack.md +30 -0
  105. package/project-templates/history/active/models.md +11 -0
  106. package/project-templates/history/changelog.md +15 -0
  107. package/project-templates/workspace/.gitkeep +0 -0
  108. package/src/cli.js +52 -0
  109. package/src/init.js +104 -0
  110. package/src/status.js +75 -0
  111. package/src/update.js +88 -0
@@ -0,0 +1,306 @@
1
+ # Skill: Authentication
2
+
3
+ ## Purpose
4
+ Implement user authentication using Rails 8's built-in authentication generator.
5
+
6
+ ## Rails 8 Authentication Generator
7
+
8
+ ### Generate Authentication
9
+ ```bash
10
+ rails generate authentication
11
+ ```
12
+
13
+ ### Generated Files
14
+ ```
15
+ app/models/user.rb
16
+ app/models/session.rb
17
+ app/models/current.rb
18
+ app/controllers/sessions_controller.rb
19
+ app/controllers/passwords_controller.rb
20
+ app/views/sessions/new.html.erb
21
+ app/views/passwords/new.html.erb
22
+ app/views/passwords/edit.html.erb
23
+ db/migrate/TIMESTAMP_create_users.rb
24
+ db/migrate/TIMESTAMP_create_sessions.rb
25
+ ```
26
+
27
+ ## User Model
28
+
29
+ ```ruby
30
+ # app/models/user.rb
31
+ class User < ApplicationRecord
32
+ has_secure_password
33
+ has_many :sessions, dependent: :destroy
34
+
35
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
36
+
37
+ validates :email_address, presence: true, uniqueness: true,
38
+ format: { with: URI::MailTo::EMAIL_REGEXP }
39
+ validates :password, length: { minimum: 8 }, allow_nil: true
40
+ end
41
+ ```
42
+
43
+ ## Session Model
44
+
45
+ ```ruby
46
+ # app/models/session.rb
47
+ class Session < ApplicationRecord
48
+ belongs_to :user
49
+
50
+ before_create do
51
+ self.token = SecureRandom.urlsafe_base64(32)
52
+ end
53
+ end
54
+ ```
55
+
56
+ ## Current Model
57
+
58
+ ```ruby
59
+ # app/models/current.rb
60
+ class Current < ActiveSupport::CurrentAttributes
61
+ attribute :session
62
+ delegate :user, to: :session, allow_nil: true
63
+ end
64
+ ```
65
+
66
+ ## Authentication Concern
67
+
68
+ ```ruby
69
+ # app/controllers/concerns/authentication.rb
70
+ module Authentication
71
+ extend ActiveSupport::Concern
72
+
73
+ included do
74
+ before_action :require_authentication
75
+ helper_method :signed_in?
76
+ end
77
+
78
+ class_methods do
79
+ def allow_unauthenticated_access(**options)
80
+ skip_before_action :require_authentication, **options
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def signed_in?
87
+ Current.session.present?
88
+ end
89
+
90
+ def require_authentication
91
+ resume_session || request_authentication
92
+ end
93
+
94
+ def resume_session
95
+ Current.session = find_session_by_cookie
96
+ end
97
+
98
+ def find_session_by_cookie
99
+ Session.find_by(token: cookies.signed[:session_token])
100
+ end
101
+
102
+ def request_authentication
103
+ session[:return_to_after_authenticating] = request.url
104
+ redirect_to new_session_path, alert: "Please sign in to continue."
105
+ end
106
+
107
+ def after_authentication_url
108
+ session.delete(:return_to_after_authenticating) || root_url
109
+ end
110
+
111
+ def start_new_session_for(user)
112
+ user.sessions.create!.tap do |session|
113
+ Current.session = session
114
+ cookies.signed.permanent[:session_token] = {
115
+ value: session.token,
116
+ httponly: true,
117
+ same_site: :lax
118
+ }
119
+ end
120
+ end
121
+
122
+ def terminate_session
123
+ Current.session.destroy
124
+ cookies.delete(:session_token)
125
+ end
126
+ end
127
+ ```
128
+
129
+ ## Sessions Controller
130
+
131
+ ```ruby
132
+ # app/controllers/sessions_controller.rb
133
+ class SessionsController < ApplicationController
134
+ allow_unauthenticated_access only: %i[new create]
135
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> {
136
+ redirect_to new_session_url, alert: "Too many attempts. Try again later."
137
+ }
138
+
139
+ def new
140
+ end
141
+
142
+ def create
143
+ if user = User.authenticate_by(email_address: params[:email_address], password: params[:password])
144
+ start_new_session_for user
145
+ redirect_to after_authentication_url, notice: "Signed in successfully."
146
+ else
147
+ redirect_to new_session_path, alert: "Invalid email or password."
148
+ end
149
+ end
150
+
151
+ def destroy
152
+ terminate_session
153
+ redirect_to new_session_path, notice: "Signed out successfully."
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Registration
159
+
160
+ ### Add Registration Controller
161
+ ```ruby
162
+ # app/controllers/registrations_controller.rb
163
+ class RegistrationsController < ApplicationController
164
+ allow_unauthenticated_access
165
+
166
+ def new
167
+ @user = User.new
168
+ end
169
+
170
+ def create
171
+ @user = User.new(user_params)
172
+
173
+ if @user.save
174
+ start_new_session_for @user
175
+ redirect_to root_path, notice: "Welcome! Your account has been created."
176
+ else
177
+ render :new, status: :unprocessable_entity
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def user_params
184
+ params.require(:user).permit(:email_address, :password, :password_confirmation)
185
+ end
186
+ end
187
+ ```
188
+
189
+ ### Registration View
190
+ ```erb
191
+ <%# app/views/registrations/new.html.erb %>
192
+ <div class="max-w-md mx-auto mt-10">
193
+ <h1 class="text-2xl font-bold mb-6">Create Account</h1>
194
+
195
+ <%= form_with model: @user, url: registration_path, class: "space-y-4" do |f| %>
196
+ <% if @user.errors.any? %>
197
+ <div class="bg-red-50 border border-red-200 rounded p-4">
198
+ <ul class="list-disc list-inside text-red-700 text-sm">
199
+ <% @user.errors.full_messages.each do |error| %>
200
+ <li><%= error %></li>
201
+ <% end %>
202
+ </ul>
203
+ </div>
204
+ <% end %>
205
+
206
+ <div>
207
+ <%= f.label :email_address, class: "block text-sm font-medium text-gray-700" %>
208
+ <%= f.email_field :email_address, required: true, autofocus: true,
209
+ class: "mt-1 w-full rounded-lg border-gray-300" %>
210
+ </div>
211
+
212
+ <div>
213
+ <%= f.label :password, class: "block text-sm font-medium text-gray-700" %>
214
+ <%= f.password_field :password, required: true,
215
+ class: "mt-1 w-full rounded-lg border-gray-300" %>
216
+ </div>
217
+
218
+ <div>
219
+ <%= f.label :password_confirmation, class: "block text-sm font-medium text-gray-700" %>
220
+ <%= f.password_field :password_confirmation, required: true,
221
+ class: "mt-1 w-full rounded-lg border-gray-300" %>
222
+ </div>
223
+
224
+ <%= f.submit "Create Account",
225
+ class: "w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg" %>
226
+ <% end %>
227
+
228
+ <p class="mt-4 text-center text-sm text-gray-600">
229
+ Already have an account?
230
+ <%= link_to "Sign in", new_session_path, class: "text-blue-600 hover:underline" %>
231
+ </p>
232
+ </div>
233
+ ```
234
+
235
+ ### Routes
236
+ ```ruby
237
+ # config/routes.rb
238
+ Rails.application.routes.draw do
239
+ resource :session, only: [:new, :create, :destroy]
240
+ resource :registration, only: [:new, :create]
241
+ resource :password, only: [:new, :create, :edit, :update]
242
+
243
+ # ...
244
+ end
245
+ ```
246
+
247
+ ## Login View
248
+
249
+ ```erb
250
+ <%# app/views/sessions/new.html.erb %>
251
+ <div class="max-w-md mx-auto mt-10">
252
+ <h1 class="text-2xl font-bold mb-6">Sign In</h1>
253
+
254
+ <%= form_with url: session_path, class: "space-y-4" do |f| %>
255
+ <div>
256
+ <%= f.label :email_address, class: "block text-sm font-medium text-gray-700" %>
257
+ <%= f.email_field :email_address, required: true, autofocus: true,
258
+ class: "mt-1 w-full rounded-lg border-gray-300" %>
259
+ </div>
260
+
261
+ <div>
262
+ <%= f.label :password, class: "block text-sm font-medium text-gray-700" %>
263
+ <%= f.password_field :password, required: true,
264
+ class: "mt-1 w-full rounded-lg border-gray-300" %>
265
+ </div>
266
+
267
+ <%= f.submit "Sign In",
268
+ class: "w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg" %>
269
+ <% end %>
270
+
271
+ <div class="mt-4 text-center text-sm text-gray-600">
272
+ <%= link_to "Forgot password?", new_password_path, class: "text-blue-600 hover:underline" %>
273
+ <span class="mx-2">|</span>
274
+ <%= link_to "Create account", new_registration_path, class: "text-blue-600 hover:underline" %>
275
+ </div>
276
+ </div>
277
+ ```
278
+
279
+ ## Application Controller
280
+
281
+ ```ruby
282
+ # app/controllers/application_controller.rb
283
+ class ApplicationController < ActionController::Base
284
+ include Authentication
285
+ end
286
+ ```
287
+
288
+ ## Current User Helper
289
+
290
+ ```erb
291
+ <%# In views %>
292
+ <% if signed_in? %>
293
+ <span>Welcome, <%= Current.user.email_address %></span>
294
+ <%= button_to "Sign Out", session_path, method: :delete %>
295
+ <% else %>
296
+ <%= link_to "Sign In", new_session_path %>
297
+ <% end %>
298
+ ```
299
+
300
+ ## Security Best Practices
301
+
302
+ 1. **Rate limiting** - Prevent brute force attacks
303
+ 2. **Secure cookies** - HttpOnly, SameSite, Secure in production
304
+ 3. **Password requirements** - Minimum 8 characters
305
+ 4. **Session management** - Token-based, revocable
306
+ 5. **Email normalization** - Consistent email handling
@@ -0,0 +1,388 @@
1
+ # Skill: Authorization
2
+
3
+ ## Purpose
4
+ Implement role-based access control using Pundit for fine-grained authorization.
5
+
6
+ ## Setup
7
+
8
+ ### Install Pundit
9
+ ```ruby
10
+ # Gemfile
11
+ gem "pundit"
12
+ ```
13
+
14
+ ```bash
15
+ bundle install
16
+ rails generate pundit:install
17
+ ```
18
+
19
+ ### Include in Application Controller
20
+ ```ruby
21
+ # app/controllers/application_controller.rb
22
+ class ApplicationController < ActionController::Base
23
+ include Pundit::Authorization
24
+
25
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
26
+
27
+ private
28
+
29
+ def user_not_authorized
30
+ flash[:alert] = "You are not authorized to perform this action."
31
+ redirect_back(fallback_location: root_path)
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## User Roles
37
+
38
+ ### Add Role to User
39
+ ```ruby
40
+ # Migration
41
+ class AddRoleToUsers < ActiveRecord::Migration[8.0]
42
+ def change
43
+ add_column :users, :role, :string, default: "user", null: false
44
+ add_index :users, :role
45
+ end
46
+ end
47
+ ```
48
+
49
+ ### User Model with Roles
50
+ ```ruby
51
+ # app/models/user.rb
52
+ class User < ApplicationRecord
53
+ ROLES = %w[user admin moderator].freeze
54
+
55
+ validates :role, inclusion: { in: ROLES }
56
+
57
+ def admin?
58
+ role == "admin"
59
+ end
60
+
61
+ def moderator?
62
+ role == "moderator"
63
+ end
64
+
65
+ def at_least_moderator?
66
+ admin? || moderator?
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Policies
72
+
73
+ ### Application Policy (Base)
74
+ ```ruby
75
+ # app/policies/application_policy.rb
76
+ class ApplicationPolicy
77
+ attr_reader :user, :record
78
+
79
+ def initialize(user, record)
80
+ @user = user
81
+ @record = record
82
+ end
83
+
84
+ def index?
85
+ true
86
+ end
87
+
88
+ def show?
89
+ true
90
+ end
91
+
92
+ def create?
93
+ user.present?
94
+ end
95
+
96
+ def new?
97
+ create?
98
+ end
99
+
100
+ def update?
101
+ owner_or_admin?
102
+ end
103
+
104
+ def edit?
105
+ update?
106
+ end
107
+
108
+ def destroy?
109
+ owner_or_admin?
110
+ end
111
+
112
+ private
113
+
114
+ def owner?
115
+ record.respond_to?(:user) && record.user == user
116
+ end
117
+
118
+ def owner_or_admin?
119
+ owner? || user&.admin?
120
+ end
121
+
122
+ def admin?
123
+ user&.admin?
124
+ end
125
+
126
+ class Scope
127
+ def initialize(user, scope)
128
+ @user = user
129
+ @scope = scope
130
+ end
131
+
132
+ def resolve
133
+ scope.all
134
+ end
135
+
136
+ private
137
+
138
+ attr_reader :user, :scope
139
+ end
140
+ end
141
+ ```
142
+
143
+ ### Resource Policy
144
+ ```ruby
145
+ # app/policies/article_policy.rb
146
+ class ArticlePolicy < ApplicationPolicy
147
+ def index?
148
+ true
149
+ end
150
+
151
+ def show?
152
+ record.published? || owner_or_admin?
153
+ end
154
+
155
+ def create?
156
+ user.present?
157
+ end
158
+
159
+ def update?
160
+ owner_or_admin?
161
+ end
162
+
163
+ def destroy?
164
+ owner_or_admin?
165
+ end
166
+
167
+ def publish?
168
+ owner_or_admin?
169
+ end
170
+
171
+ class Scope < ApplicationPolicy::Scope
172
+ def resolve
173
+ if user&.admin?
174
+ scope.all
175
+ elsif user
176
+ scope.where(published: true).or(scope.where(user: user))
177
+ else
178
+ scope.where(published: true)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ ```
184
+
185
+ ### Admin-Only Policy
186
+ ```ruby
187
+ # app/policies/admin/user_policy.rb
188
+ module Admin
189
+ class UserPolicy < ApplicationPolicy
190
+ def index?
191
+ admin?
192
+ end
193
+
194
+ def show?
195
+ admin?
196
+ end
197
+
198
+ def update?
199
+ admin? && record != user # Can't edit self
200
+ end
201
+
202
+ def destroy?
203
+ admin? && record != user # Can't delete self
204
+ end
205
+ end
206
+ end
207
+ ```
208
+
209
+ ## Controller Usage
210
+
211
+ ### Basic Authorization
212
+ ```ruby
213
+ class ArticlesController < ApplicationController
214
+ def show
215
+ @article = Article.find(params[:id])
216
+ authorize @article
217
+ end
218
+
219
+ def create
220
+ @article = current_user.articles.build(article_params)
221
+ authorize @article
222
+
223
+ if @article.save
224
+ redirect_to @article
225
+ else
226
+ render :new, status: :unprocessable_entity
227
+ end
228
+ end
229
+
230
+ def update
231
+ @article = Article.find(params[:id])
232
+ authorize @article
233
+
234
+ if @article.update(article_params)
235
+ redirect_to @article
236
+ else
237
+ render :edit, status: :unprocessable_entity
238
+ end
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### Policy Scopes
244
+ ```ruby
245
+ class ArticlesController < ApplicationController
246
+ def index
247
+ @articles = policy_scope(Article).recent
248
+ end
249
+ end
250
+ ```
251
+
252
+ ### Authorize in Before Action
253
+ ```ruby
254
+ class ArticlesController < ApplicationController
255
+ before_action :set_article, only: [:show, :edit, :update, :destroy]
256
+
257
+ def edit
258
+ # authorize called in set_article
259
+ end
260
+
261
+ private
262
+
263
+ def set_article
264
+ @article = Article.find(params[:id])
265
+ authorize @article
266
+ end
267
+ end
268
+ ```
269
+
270
+ ## View Helpers
271
+
272
+ ### Conditional Display
273
+ ```erb
274
+ <% if policy(@article).edit? %>
275
+ <%= link_to "Edit", edit_article_path(@article) %>
276
+ <% end %>
277
+
278
+ <% if policy(@article).destroy? %>
279
+ <%= button_to "Delete", @article, method: :delete %>
280
+ <% end %>
281
+ ```
282
+
283
+ ### New Record Check
284
+ ```erb
285
+ <% if policy(Article).create? %>
286
+ <%= link_to "New Article", new_article_path %>
287
+ <% end %>
288
+ ```
289
+
290
+ ### Admin-Only Section
291
+ ```erb
292
+ <% if policy([:admin, User]).index? %>
293
+ <%= link_to "Manage Users", admin_users_path %>
294
+ <% end %>
295
+ ```
296
+
297
+ ## Headless Policies
298
+
299
+ For actions not tied to a model:
300
+
301
+ ```ruby
302
+ # app/policies/dashboard_policy.rb
303
+ class DashboardPolicy < ApplicationPolicy
304
+ def initialize(user, _record = nil)
305
+ @user = user
306
+ end
307
+
308
+ def show?
309
+ user.present?
310
+ end
311
+
312
+ def admin?
313
+ user&.admin?
314
+ end
315
+ end
316
+ ```
317
+
318
+ ```ruby
319
+ # Controller
320
+ class DashboardController < ApplicationController
321
+ def show
322
+ authorize :dashboard, :show?
323
+ end
324
+
325
+ def admin
326
+ authorize :dashboard, :admin?
327
+ end
328
+ end
329
+ ```
330
+
331
+ ## Testing Policies
332
+
333
+ ```ruby
334
+ # spec/policies/article_policy_spec.rb
335
+ require "rails_helper"
336
+
337
+ RSpec.describe ArticlePolicy do
338
+ subject { described_class }
339
+
340
+ let(:user) { create(:user) }
341
+ let(:admin) { create(:user, role: "admin") }
342
+ let(:article) { create(:article, user: user) }
343
+ let(:other_article) { create(:article) }
344
+
345
+ permissions :update?, :destroy? do
346
+ it "allows owner" do
347
+ expect(subject).to permit(user, article)
348
+ end
349
+
350
+ it "denies non-owner" do
351
+ expect(subject).not_to permit(user, other_article)
352
+ end
353
+
354
+ it "allows admin" do
355
+ expect(subject).to permit(admin, other_article)
356
+ end
357
+ end
358
+
359
+ describe "Scope" do
360
+ let!(:published) { create(:article, published: true) }
361
+ let!(:draft) { create(:article, published: false) }
362
+ let!(:own_draft) { create(:article, published: false, user: user) }
363
+
364
+ it "shows only published to guests" do
365
+ scope = ArticlePolicy::Scope.new(nil, Article).resolve
366
+ expect(scope).to contain_exactly(published)
367
+ end
368
+
369
+ it "shows published + own drafts to users" do
370
+ scope = ArticlePolicy::Scope.new(user, Article).resolve
371
+ expect(scope).to contain_exactly(published, own_draft)
372
+ end
373
+
374
+ it "shows all to admin" do
375
+ scope = ArticlePolicy::Scope.new(admin, Article).resolve
376
+ expect(scope).to contain_exactly(published, draft, own_draft)
377
+ end
378
+ end
379
+ end
380
+ ```
381
+
382
+ ## Best Practices
383
+
384
+ 1. **Authorize early** - Call authorize at the start of actions
385
+ 2. **Use scopes** - Filter collections with policy_scope
386
+ 3. **Test policies** - Write specs for all permission scenarios
387
+ 4. **Keep policies focused** - One policy per resource
388
+ 5. **Use permitted_attributes** - Let policy control params