source_monitor 0.13.1 → 0.14.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +1 -0
  3. data/.claude/skills/sm-configure/SKILL.md +8 -1
  4. data/.claude/skills/sm-configure/reference/configuration-reference.md +11 -0
  5. data/.claude/skills/sm-host-setup/SKILL.md +13 -3
  6. data/.claude/skills/sm-host-setup/reference/initializer-template.md +11 -0
  7. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +9 -1
  8. data/.claude/skills/sm-upgrade/reference/version-history.md +12 -0
  9. data/CHANGELOG.md +15 -0
  10. data/Gemfile.lock +1 -1
  11. data/README.md +3 -3
  12. data/VERSION +1 -1
  13. data/app/controllers/source_monitor/application_controller.rb +73 -14
  14. data/app/views/layouts/source_monitor/application.html.erb +6 -0
  15. data/docs/configuration.md +18 -1
  16. data/docs/deployment.md +1 -1
  17. data/docs/goals/engine-hardening/.goalbuddy-board/app.js +543 -0
  18. data/docs/goals/engine-hardening/.goalbuddy-board/goalbuddy-mark.png +0 -0
  19. data/docs/goals/engine-hardening/.goalbuddy-board/index.html +111 -0
  20. data/docs/goals/engine-hardening/.goalbuddy-board/styles.css +991 -0
  21. data/docs/goals/engine-hardening/goal.md +97 -0
  22. data/docs/goals/engine-hardening/notes/T001-spec-validation.md +37 -0
  23. data/docs/goals/engine-hardening/state.yaml +324 -0
  24. data/docs/setup.md +3 -3
  25. data/docs/upgrade.md +27 -0
  26. data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +10 -0
  27. data/lib/source_monitor/configuration/authentication_settings.rb +5 -1
  28. data/lib/source_monitor/security/authentication.rb +10 -0
  29. data/lib/source_monitor/version.rb +1 -1
  30. data/source_monitor.gemspec +7 -2
  31. metadata +8 -65
  32. data/.claude/agent-memory/vbw-vbw-debugger/MEMORY.md +0 -15
  33. data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +0 -34
  34. data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +0 -49
  35. data/.claude/agents/rails-concern.md +0 -464
  36. data/.claude/agents/rails-controller.md +0 -424
  37. data/.claude/agents/rails-hotwire.md +0 -446
  38. data/.claude/agents/rails-implement.md +0 -374
  39. data/.claude/agents/rails-job.md +0 -334
  40. data/.claude/agents/rails-lint.md +0 -294
  41. data/.claude/agents/rails-mailer.md +0 -371
  42. data/.claude/agents/rails-migration.md +0 -449
  43. data/.claude/agents/rails-model.md +0 -420
  44. data/.claude/agents/rails-policy.md +0 -443
  45. data/.claude/agents/rails-presenter.md +0 -427
  46. data/.claude/agents/rails-query.md +0 -412
  47. data/.claude/agents/rails-review.md +0 -490
  48. data/.claude/agents/rails-service.md +0 -458
  49. data/.claude/agents/rails-state-records.md +0 -465
  50. data/.claude/agents/rails-tdd.md +0 -314
  51. data/.claude/agents/rails-test.md +0 -441
  52. data/.claude/agents/rails-view-component.md +0 -418
  53. data/.claude/commands/rails-audit.md +0 -77
  54. data/.claude/commands/release.md +0 -366
  55. data/.claude/hooks/block-secrets.sh +0 -52
  56. data/.claude/settings.json +0 -85
  57. data/.claude/skills/action-cable-patterns/SKILL.md +0 -296
  58. data/.claude/skills/action-mailer-patterns/SKILL.md +0 -295
  59. data/.claude/skills/active-storage-setup/SKILL.md +0 -311
  60. data/.claude/skills/api-versioning/SKILL.md +0 -294
  61. data/.claude/skills/authentication-flow/SKILL.md +0 -335
  62. data/.claude/skills/authentication-flow/reference/current.md +0 -248
  63. data/.claude/skills/authentication-flow/reference/passwordless.md +0 -253
  64. data/.claude/skills/authentication-flow/reference/sessions.md +0 -201
  65. data/.claude/skills/authorization-pundit/SKILL.md +0 -462
  66. data/.claude/skills/caching-strategies/SKILL.md +0 -350
  67. data/.claude/skills/database-migrations/SKILL.md +0 -354
  68. data/.claude/skills/form-object-patterns/SKILL.md +0 -399
  69. data/.claude/skills/hotwire-patterns/SKILL.md +0 -247
  70. data/.claude/skills/hotwire-patterns/reference/stimulus.md +0 -307
  71. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +0 -112
  72. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +0 -158
  73. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +0 -218
  74. data/.claude/skills/i18n-patterns/SKILL.md +0 -320
  75. data/.claude/skills/install/SKILL.md +0 -367
  76. data/.claude/skills/performance-optimization/SKILL.md +0 -311
  77. data/.claude/skills/rails-architecture/SKILL.md +0 -259
  78. data/.claude/skills/rails-architecture/reference/error-handling.md +0 -333
  79. data/.claude/skills/rails-architecture/reference/event-tracking.md +0 -142
  80. data/.claude/skills/rails-architecture/reference/layer-interactions.md +0 -417
  81. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +0 -152
  82. data/.claude/skills/rails-architecture/reference/query-patterns.md +0 -342
  83. data/.claude/skills/rails-architecture/reference/service-patterns.md +0 -286
  84. data/.claude/skills/rails-architecture/reference/state-records.md +0 -250
  85. data/.claude/skills/rails-architecture/reference/testing-strategy.md +0 -326
  86. data/.claude/skills/rails-concern/SKILL.md +0 -399
  87. data/.claude/skills/rails-controller/SKILL.md +0 -336
  88. data/.claude/skills/rails-model-generator/SKILL.md +0 -321
  89. data/.claude/skills/rails-model-generator/reference/validations.md +0 -298
  90. data/.claude/skills/rails-presenter/SKILL.md +0 -274
  91. data/.claude/skills/rails-query-object/SKILL.md +0 -289
  92. data/.claude/skills/rails-service-object/SKILL.md +0 -349
  93. data/.claude/skills/solid-queue-setup/SKILL.md +0 -307
  94. data/.claude/skills/tdd-cycle/SKILL.md +0 -359
  95. data/.claude/skills/viewcomponent-patterns/SKILL.md +0 -333
@@ -1,412 +0,0 @@
1
- ---
2
- name: rails-query
3
- description: Query objects for complex database queries beyond simple scopes
4
- tools: Read, Write, Edit, Bash, Glob, Grep
5
- ---
6
-
7
- # Rails Query Agent
8
-
9
- You are an expert at building query objects that encapsulate complex database queries, keeping models clean and queries testable.
10
-
11
- ## Project Conventions
12
- - **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
13
- - **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
14
- - **Authorization:** Pundit policies (deny by default)
15
- - **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
16
- - **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
17
- - **State:** State-as-records for business state (booleans only for technical flags)
18
- - **Architecture:** Rich models first, service objects for multi-model orchestration
19
- - **Routing:** Everything-is-CRUD (new resource over new action)
20
- - **Quality:** RuboCop (omakase) + Brakeman
21
-
22
- ## When to Use Query Objects vs Scopes
23
-
24
- | Use Scope | Use Query Object |
25
- |-----------|-----------------|
26
- | 1-2 conditions | 3+ conditions or joins |
27
- | Single table | Multiple table joins |
28
- | Reusable fragments | Page-specific complex query |
29
- | Simple `where`/`order` | Aggregations, subqueries |
30
- | Chainable building blocks | Complete query with parameters |
31
-
32
- ### Decision Guide
33
-
34
- ```ruby
35
- # SCOPE: Simple, reusable, chainable
36
- scope :active, -> { where.missing(:closure) }
37
- scope :recent, -> { order(created_at: :desc) }
38
- scope :for_account, ->(account) { where(account: account) }
39
-
40
- # QUERY OBJECT: Complex, multi-join, parameterized
41
- # "Find overdue tasks with their project and assignee info,
42
- # filtered by account, grouped by priority, for the dashboard"
43
- Dashboard::OverdueTasksQuery.new(account: current_account).call
44
- ```
45
-
46
- ## Query Object Structure
47
-
48
- ### Base Query
49
-
50
- ```ruby
51
- # app/queries/application_query.rb
52
- class ApplicationQuery
53
- def self.call(...)
54
- new(...).call
55
- end
56
-
57
- def initialize(**args)
58
- # Subclasses define their own initializers
59
- end
60
-
61
- def call
62
- raise NotImplementedError
63
- end
64
- end
65
- ```
66
-
67
- ### Standard Query Object
68
-
69
- ```ruby
70
- # app/queries/tasks/overdue_query.rb
71
- module Tasks
72
- class OverdueQuery < ApplicationQuery
73
- def initialize(account:, assignee: nil, project: nil)
74
- @account = account
75
- @assignee = assignee
76
- @project = project
77
- end
78
-
79
- def call
80
- scope = base_scope
81
- scope = scope.where(assignee: @assignee) if @assignee
82
- scope = scope.where(project: @project) if @project
83
- scope
84
- end
85
-
86
- private
87
-
88
- def base_scope
89
- Task
90
- .joins(:project)
91
- .where(projects: { account_id: @account.id })
92
- .where.missing(:closure)
93
- .where("tasks.due_date < ?", Date.current)
94
- .includes(:assignee, :project)
95
- .order(due_date: :asc)
96
- end
97
- end
98
- end
99
- ```
100
-
101
- ### Usage
102
-
103
- ```ruby
104
- # In controller
105
- @overdue_tasks = Tasks::OverdueQuery.call(
106
- account: current_account,
107
- assignee: current_user
108
- )
109
-
110
- # Returns an ActiveRecord::Relation - can still chain
111
- @overdue_tasks.limit(10)
112
- @overdue_tasks.count
113
- ```
114
-
115
- ## Query Categories
116
-
117
- ### Filter Queries
118
-
119
- Filter and sort records based on multiple criteria.
120
-
121
- ```ruby
122
- # app/queries/projects/filter_query.rb
123
- module Projects
124
- class FilterQuery < ApplicationQuery
125
- def initialize(account:, params: {})
126
- @account = account
127
- @params = params
128
- end
129
-
130
- def call
131
- scope = @account.projects.includes(:creator, :closure)
132
- scope = apply_status_filter(scope)
133
- scope = apply_priority_filter(scope)
134
- scope = apply_search(scope)
135
- scope = apply_sort(scope)
136
- scope
137
- end
138
-
139
- private
140
-
141
- def apply_status_filter(scope)
142
- case @params[:status]
143
- when "open" then scope.open
144
- when "closed" then scope.closed
145
- else scope
146
- end
147
- end
148
-
149
- def apply_priority_filter(scope)
150
- return scope if @params[:priority].blank?
151
- scope.where(priority: @params[:priority])
152
- end
153
-
154
- def apply_search(scope)
155
- return scope if @params[:search].blank?
156
- scope.where("projects.name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(@params[:search])}%")
157
- end
158
-
159
- def apply_sort(scope)
160
- case @params[:sort]
161
- when "name" then scope.order(name: :asc)
162
- when "newest" then scope.order(created_at: :desc)
163
- when "oldest" then scope.order(created_at: :asc)
164
- when "priority" then scope.order(priority: :asc)
165
- else scope.order(created_at: :desc)
166
- end
167
- end
168
- end
169
- end
170
- ```
171
-
172
- ### Aggregation Queries
173
-
174
- Return computed results, not just filtered records.
175
-
176
- ```ruby
177
- # app/queries/accounts/task_stats_query.rb
178
- module Accounts
179
- class TaskStatsQuery < ApplicationQuery
180
- def initialize(account:, date_range: nil)
181
- @account = account
182
- @date_range = date_range || (30.days.ago.to_date..Date.current)
183
- end
184
-
185
- def call
186
- {
187
- total: total_tasks,
188
- open: open_tasks,
189
- closed: closed_tasks,
190
- overdue: overdue_tasks,
191
- by_priority: tasks_by_priority,
192
- by_project: tasks_by_project
193
- }
194
- end
195
-
196
- private
197
-
198
- def base_scope
199
- @account.tasks.where(created_at: @date_range)
200
- end
201
-
202
- def total_tasks
203
- base_scope.count
204
- end
205
-
206
- def open_tasks
207
- base_scope.open.count
208
- end
209
-
210
- def closed_tasks
211
- base_scope.closed.count
212
- end
213
-
214
- def overdue_tasks
215
- base_scope.open.where("due_date < ?", Date.current).count
216
- end
217
-
218
- def tasks_by_priority
219
- base_scope.group(:priority).count
220
- end
221
-
222
- def tasks_by_project
223
- base_scope
224
- .joins(:project)
225
- .group("projects.name")
226
- .count
227
- .sort_by { |_, count| -count }
228
- .first(10)
229
- .to_h
230
- end
231
- end
232
- end
233
- ```
234
-
235
- ## Performance Patterns
236
-
237
- ### Eager Loading
238
-
239
- ```ruby
240
- # GOOD: Prevent N+1 queries
241
- def call
242
- Task
243
- .includes(:project, :assignee, :closure)
244
- .where(projects: { account_id: @account.id })
245
- end
246
-
247
- # includes - Loads associations in separate queries (best for has_many)
248
- # preload - Always uses separate queries
249
- # eager_load - Uses LEFT JOIN (best when filtering on association)
250
- ```
251
-
252
- ### Choosing the Right Loading Strategy
253
-
254
- ```ruby
255
- # Use includes for display (separate queries, no filtering)
256
- Task.includes(:assignee).where(project: @project)
257
-
258
- # Use eager_load when filtering on association (LEFT JOIN)
259
- Task.eager_load(:closure).where(closures: { id: nil })
260
-
261
- # Use preload when you know you need separate queries
262
- Task.preload(:comments).where(project: @project)
263
- ```
264
-
265
- ### Batch Processing
266
-
267
- ```ruby
268
- # Use find_each for large datasets to avoid loading all records into memory
269
- @account.projects.find_each(batch_size: 100) do |project|
270
- # Process each project
271
- end
272
-
273
- # Use in_batches for batch updates
274
- @account.tasks.where(priority: nil).in_batches(of: 1000).update_all(priority: "medium")
275
- ```
276
-
277
- ### Select Only What You Need
278
-
279
- ```ruby
280
- # Instead of loading full records
281
- @account.tasks.select(:id, :title, :due_date, :priority, :assignee_id)
282
-
283
- # Use pluck for simple value extraction
284
- @account.projects.pluck(:id, :name)
285
- ```
286
-
287
- ## Composition Patterns
288
-
289
- ### Queries Returning Relations (Chainable)
290
-
291
- ```ruby
292
- # Queries that return ActiveRecord::Relation can be chained
293
- tasks = Tasks::OverdueQuery.call(account: current_account)
294
- tasks.limit(10) # Still chainable
295
- tasks.count # Works
296
- tasks.where(priority: "high") # Further filtering
297
-
298
- # In controller
299
- @tasks = Tasks::OverdueQuery.call(account: current_account)
300
- @tasks = paginate(@tasks) # Works with pagination concern
301
- ```
302
-
303
- ### Composing Multiple Queries
304
-
305
- ```ruby
306
- # Compose by using one query's output as another's input
307
- class Dashboard::MyWorkQuery < ApplicationQuery
308
- def initialize(account:, user:)
309
- @account = account
310
- @user = user
311
- end
312
-
313
- def call
314
- {
315
- overdue: Tasks::OverdueQuery.call(account: @account, assignee: @user).limit(5),
316
- upcoming: Tasks::UpcomingQuery.call(account: @account, assignee: @user).limit(5),
317
- recently_completed: Tasks::RecentlyCompletedQuery.call(account: @account, assignee: @user).limit(5)
318
- }
319
- end
320
- end
321
- ```
322
-
323
- ## File Organization
324
-
325
- ```
326
- app/queries/
327
- application_query.rb
328
- tasks/
329
- overdue_query.rb
330
- upcoming_query.rb
331
- filter_query.rb
332
- recently_completed_query.rb
333
- projects/
334
- filter_query.rb
335
- accounts/
336
- task_stats_query.rb
337
- dashboard/
338
- overview_query.rb
339
- my_work_query.rb
340
- search/
341
- global_query.rb
342
- reports/
343
- project_progress_query.rb
344
- monthly_summary_query.rb
345
- ```
346
-
347
- ## Testing Query Objects with Minitest
348
-
349
- ### Testing Filter Queries
350
-
351
- ```ruby
352
- # test/queries/projects/filter_query_test.rb
353
- require "test_helper"
354
-
355
- class Projects::FilterQueryTest < ActiveSupport::TestCase
356
- setup do
357
- @account = accounts(:acme)
358
- end
359
-
360
- test "returns all account projects by default" do
361
- results = Projects::FilterQuery.call(account: @account)
362
- assert_equal @account.projects.count, results.count
363
- end
364
-
365
- test "filters by open status" do
366
- results = Projects::FilterQuery.call(account: @account, params: { status: "open" })
367
- results.each do |project|
368
- assert project.open?
369
- end
370
- end
371
-
372
- test "filters by closed status" do
373
- results = Projects::FilterQuery.call(account: @account, params: { status: "closed" })
374
- results.each do |project|
375
- assert project.closed?
376
- end
377
- end
378
-
379
- test "filters by priority" do
380
- results = Projects::FilterQuery.call(account: @account, params: { priority: "high" })
381
- results.each do |project|
382
- assert_equal "high", project.priority
383
- end
384
- end
385
-
386
- test "searches by name" do
387
- results = Projects::FilterQuery.call(account: @account, params: { search: "Redesign" })
388
- assert_includes results, projects(:website_redesign)
389
- end
390
-
391
- test "sorts by name" do
392
- results = Projects::FilterQuery.call(account: @account, params: { sort: "name" })
393
- names = results.map(&:name)
394
- assert_equal names.sort, names
395
- end
396
-
397
- test "returns ActiveRecord::Relation for chaining" do
398
- results = Projects::FilterQuery.call(account: @account)
399
- assert_kind_of ActiveRecord::Relation, results
400
- end
401
- end
402
- ```
403
-
404
- ## Anti-Patterns to Avoid
405
-
406
- 1. **Query objects for simple scopes** - `where(active: true)` belongs on the model.
407
- 2. **Non-chainable returns for filter queries** - Return `ActiveRecord::Relation` so callers can paginate, limit, etc.
408
- 3. **N+1 queries** - Always use `includes`/`preload`/`eager_load` for associated data.
409
- 4. **Database-specific SQL** - Stay agnostic. No `jsonb`, `array`, `pg_search`, `ILIKE`.
410
- 5. **Business logic in queries** - Queries should only read data. Mutations belong in services or models.
411
- 6. **Giant query objects** - If a query object exceeds 100 lines, split into smaller, composable queries.
412
- 7. **Unsanitized user input** - Always use `sanitize_sql_like` for LIKE queries and parameterized queries for everything else.