rubyn-code 0.1.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 (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,280 @@
1
+ # Rails: Model Concerns
2
+
3
+ ## Pattern
4
+
5
+ Use model concerns for genuine, reusable behavior that multiple unrelated models need. A good concern adds one well-defined capability — slugging, soft deleting, auditing, searching. It has a clear contract and works without knowledge of the including model's specific attributes.
6
+
7
+ ```ruby
8
+ # app/models/concerns/searchable.rb
9
+ module Searchable
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ scope :search, ->(query) {
14
+ return all if query.blank?
15
+
16
+ columns = searchable_columns.map { |col| "#{table_name}.#{col}" }
17
+ conditions = columns.map { |col| "#{col} ILIKE :query" }.join(" OR ")
18
+ where(conditions, query: "%#{sanitize_sql_like(query)}%")
19
+ }
20
+ end
21
+
22
+ class_methods do
23
+ def searchable_columns
24
+ raise NotImplementedError, "#{name} must define searchable_columns"
25
+ end
26
+ end
27
+ end
28
+
29
+ # Usage in models — each defines what's searchable
30
+ class User < ApplicationRecord
31
+ include Searchable
32
+
33
+ def self.searchable_columns
34
+ %w[name email]
35
+ end
36
+ end
37
+
38
+ class Product < ApplicationRecord
39
+ include Searchable
40
+
41
+ def self.searchable_columns
42
+ %w[name description sku]
43
+ end
44
+ end
45
+
46
+ # Both work identically
47
+ User.search("alice")
48
+ Product.search("widget")
49
+ ```
50
+
51
+ ```ruby
52
+ # app/models/concerns/soft_deletable.rb
53
+ module SoftDeletable
54
+ extend ActiveSupport::Concern
55
+
56
+ included do
57
+ scope :kept, -> { where(discarded_at: nil) }
58
+ scope :discarded, -> { where.not(discarded_at: nil) }
59
+
60
+ default_scope { kept }
61
+ end
62
+
63
+ def discard
64
+ update(discarded_at: Time.current)
65
+ end
66
+
67
+ def undiscard
68
+ update(discarded_at: nil)
69
+ end
70
+
71
+ def discarded?
72
+ discarded_at.present?
73
+ end
74
+ end
75
+ ```
76
+
77
+ ```ruby
78
+ # app/models/concerns/has_token.rb
79
+ module HasToken
80
+ extend ActiveSupport::Concern
81
+
82
+ included do
83
+ before_create :generate_token
84
+ end
85
+
86
+ class_methods do
87
+ def token_column
88
+ :token
89
+ end
90
+
91
+ def find_by_token!(token)
92
+ find_by!(token_column => token)
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def generate_token
99
+ column = self.class.token_column
100
+ loop do
101
+ self[column] = SecureRandom.urlsafe_base64(32)
102
+ break unless self.class.exists?(column => self[column])
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Why This Is Good
109
+
110
+ - **Genuinely reusable.** `Searchable`, `SoftDeletable`, and `HasToken` work on any model. They don't know or care about order-specific, user-specific, or product-specific logic.
111
+ - **Clear contract.** `Searchable` requires the model to define `searchable_columns`. The concern raises `NotImplementedError` if the model forgets. The contract is explicit and enforced.
112
+ - **Self-contained.** Including `SoftDeletable` gives you scopes, instance methods, and a default scope. The model doesn't need to configure anything — just include and add a `discarded_at` column.
113
+ - **Tested independently.** You can write a shared example that tests the searchable behavior, then include it in User and Product specs. One test verifies the concern works; per-model tests verify the configuration.
114
+ - **Namespace isolation.** The concern defines behavior. The model defines which columns/attributes to apply it to. Neither reaches into the other's internals.
115
+
116
+ ## Anti-Pattern
117
+
118
+ Using concerns to split a fat model into multiple files without actually improving the design:
119
+
120
+ ```ruby
121
+ # app/models/concerns/order_calculations.rb
122
+ module OrderCalculations
123
+ extend ActiveSupport::Concern
124
+
125
+ def calculate_subtotal
126
+ line_items.sum { |li| li.quantity * li.unit_price }
127
+ end
128
+
129
+ def calculate_tax
130
+ subtotal * tax_rate
131
+ end
132
+
133
+ def calculate_shipping
134
+ return 0 if subtotal > 100
135
+ line_items.sum(&:weight) * 0.5
136
+ end
137
+
138
+ def calculate_total
139
+ calculate_subtotal + calculate_tax + calculate_shipping
140
+ end
141
+
142
+ def apply_discount(code)
143
+ discount = Discount.find_by(code: code)
144
+ self.discount_amount = discount&.calculate(calculate_subtotal) || 0
145
+ end
146
+ end
147
+
148
+ # app/models/concerns/order_status.rb
149
+ module OrderStatus
150
+ extend ActiveSupport::Concern
151
+
152
+ included do
153
+ enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3, cancelled: 4 }
154
+
155
+ after_update :handle_status_change, if: :saved_change_to_status?
156
+ end
157
+
158
+ def can_cancel?
159
+ pending? || confirmed?
160
+ end
161
+
162
+ def can_ship?
163
+ confirmed? && line_items.all?(&:in_stock?)
164
+ end
165
+
166
+ private
167
+
168
+ def handle_status_change
169
+ case status
170
+ when "confirmed" then OrderMailer.confirmed(self).deliver_later
171
+ when "shipped" then OrderMailer.shipped(self).deliver_later
172
+ when "cancelled" then process_cancellation
173
+ end
174
+ end
175
+
176
+ def process_cancellation
177
+ line_items.each { |li| li.product.increment!(:stock, li.quantity) }
178
+ RefundService.call(self)
179
+ end
180
+ end
181
+
182
+ # app/models/order.rb
183
+ class Order < ApplicationRecord
184
+ include OrderCalculations
185
+ include OrderStatus
186
+ include OrderNotifications
187
+ include OrderValidations
188
+ include OrderScopes
189
+
190
+ # Model is now 5 lines but still has 500 lines of responsibility
191
+ end
192
+ ```
193
+
194
+ ## Why This Is Bad
195
+
196
+ - **Same responsibilities, different files.** The Order model still has calculations, status management, email sending, inventory management, and refund processing — they're just scattered across 5 files instead of 1. The complexity hasn't been reduced.
197
+ - **Not reusable.** `OrderCalculations` only works for orders. No other model can include it. It's not a shared capability — it's an order-specific feature hidden in a concern.
198
+ - **Harder to navigate.** A developer looking at `Order` sees 5 includes and has to open 5 files to understand what the model does. In a single file, they can scroll. With concerns, they play file hopscotch.
199
+ - **Hidden callbacks.** `OrderStatus` adds an `after_update` callback that sends emails and processes refunds. Including `OrderStatus` in the model gives you no indication that saving an order might trigger a refund.
200
+ - **Business logic in concerns.** `process_cancellation` does inventory management and calls `RefundService`. This belongs in a service object (`Orders::CancelService`), not in a model concern.
201
+
202
+ ## When To Apply
203
+
204
+ Use model concerns when ALL of these are true:
205
+
206
+ 1. **Multiple unrelated models** need the same behavior (at least 2, ideally 3+)
207
+ 2. The behavior is a **capability** ("searchable", "sluggable", "auditable"), not a **feature** ("order calculations")
208
+ 3. The concern is **self-contained** — it doesn't need to know the model's specific business logic
209
+ 4. The concern has a **clear contract** — the model must provide specific columns or methods, documented explicitly
210
+
211
+ ## When NOT To Apply
212
+
213
+ - **Don't use concerns to split a fat model.** If the model is too big, extract service objects, form objects, and query objects. Moving code to a concern file doesn't reduce complexity.
214
+ - **Don't create a concern used by one model.** That's just indirection. Keep the code in the model.
215
+ - **Don't put business logic in concerns.** Calculations, status transitions, payment processing, and notification sending are business logic. They belong in service objects.
216
+ - **Don't put callbacks with side effects in concerns.** If a concern adds `after_create :send_welcome_email`, every model that includes it gets that behavior — possibly unintentionally. Side-effect callbacks belong in service objects.
217
+
218
+ ## Edge Cases
219
+
220
+ **Concern needs different configuration per model:**
221
+ Use class methods that the model overrides:
222
+
223
+ ```ruby
224
+ module Archivable
225
+ extend ActiveSupport::Concern
226
+
227
+ class_methods do
228
+ def archive_after
229
+ 30.days # Default
230
+ end
231
+ end
232
+
233
+ included do
234
+ scope :archivable, -> { where(created_at: ..archive_after.ago) }
235
+ end
236
+ end
237
+
238
+ class Order < ApplicationRecord
239
+ include Archivable
240
+
241
+ def self.archive_after
242
+ 90.days # Override
243
+ end
244
+ end
245
+ ```
246
+
247
+ **Testing concerns:**
248
+ Use shared examples that any including model can run:
249
+
250
+ ```ruby
251
+ RSpec.shared_examples "a searchable model" do
252
+ describe ".search" do
253
+ it "finds records matching the query" do
254
+ matching = create(described_class.model_name.singular, name: "Rubyn Widget")
255
+ non_matching = create(described_class.model_name.singular, name: "Other Thing")
256
+
257
+ results = described_class.search("rubyn")
258
+ expect(results).to include(matching)
259
+ expect(results).not_to include(non_matching)
260
+ end
261
+
262
+ it "returns all records for blank query" do
263
+ create(described_class.model_name.singular)
264
+ expect(described_class.search("")).to eq(described_class.all)
265
+ end
266
+ end
267
+ end
268
+
269
+ # In each model spec
270
+ RSpec.describe User do
271
+ it_behaves_like "a searchable model"
272
+ end
273
+
274
+ RSpec.describe Product do
275
+ it_behaves_like "a searchable model"
276
+ end
277
+ ```
278
+
279
+ **Concern vs STI (Single Table Inheritance):**
280
+ Use STI when models share a database table and have an "is-a" relationship (AdminUser is a User). Use concerns when models share behavior but have separate tables and no inheritance relationship (both User and Product are searchable, but a User is not a Product).
@@ -0,0 +1,190 @@
1
+ # Rails: Skinny Controllers
2
+
3
+ ## Pattern
4
+
5
+ Controllers handle HTTP concerns only: receive params, delegate to a service or model, respond with the appropriate format and status code. Business logic, data transformation, and side effects live elsewhere.
6
+
7
+ A well-structured controller action follows this shape:
8
+
9
+ ```ruby
10
+ # app/controllers/orders_controller.rb
11
+ class OrdersController < ApplicationController
12
+ before_action :set_order, only: [:show, :update, :destroy]
13
+
14
+ def index
15
+ @orders = Current.user.orders.recent.page(params[:page])
16
+ end
17
+
18
+ def show
19
+ end
20
+
21
+ def create
22
+ result = Orders::CreateService.call(order_params, current_user)
23
+
24
+ if result.success?
25
+ redirect_to result.order, notice: "Order placed."
26
+ else
27
+ @order = result.order
28
+ render :new, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def update
33
+ if @order.update(order_params)
34
+ redirect_to @order, notice: "Order updated."
35
+ else
36
+ render :edit, status: :unprocessable_entity
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ @order.destroy
42
+ redirect_to orders_path, notice: "Order deleted."
43
+ end
44
+
45
+ private
46
+
47
+ def set_order
48
+ @order = Current.user.orders.find(params[:id])
49
+ end
50
+
51
+ def order_params
52
+ params.require(:order).permit(:shipping_address, :notes)
53
+ end
54
+ end
55
+ ```
56
+
57
+ Key principles:
58
+ - Each action is 1-5 lines of logic (excluding private methods)
59
+ - `before_action` for shared record loading
60
+ - Private methods only for param filtering and record lookup
61
+ - No business logic, no conditional branching beyond success/failure
62
+ - Delegate complex operations to service objects
63
+ - Use `Current` attributes or scoped queries — never `Order.find(params[:id])` without scoping to the user
64
+
65
+ When an action needs more than simple CRUD, add a new controller rather than a new action:
66
+
67
+ ```ruby
68
+ # Instead of orders_controller#cancel, create:
69
+ # app/controllers/order_cancellations_controller.rb
70
+ class OrderCancellationsController < ApplicationController
71
+ def create
72
+ @order = Current.user.orders.find(params[:order_id])
73
+ result = Orders::CancelService.call(@order, current_user)
74
+
75
+ if result.success?
76
+ redirect_to @order, notice: "Order cancelled."
77
+ else
78
+ redirect_to @order, alert: result.error
79
+ end
80
+ end
81
+ end
82
+
83
+ # config/routes.rb
84
+ resources :orders do
85
+ resource :cancellation, only: [:create]
86
+ end
87
+ ```
88
+
89
+ ## Why This Is Good
90
+
91
+ - **Readable at a glance.** A new developer can open any controller and understand what every endpoint does in seconds. There's no business logic to parse — just HTTP flow.
92
+ - **Testable via request specs.** Thin controllers are tested through HTTP (request specs), which tests the real behavior. No need for brittle controller unit tests.
93
+ - **Consistent across the team.** Every controller follows the same 5-line-action pattern. Code reviews are faster because the shape is predictable.
94
+ - **RESTful by design.** Adding new controllers instead of new actions keeps the app RESTful. `OrderCancellationsController#create` is clearer than `OrdersController#cancel`.
95
+ - **Forces good architecture.** When you can't put logic in the controller, you're forced to find the right home for it — service objects, models, form objects, or query objects.
96
+
97
+ ## Anti-Pattern
98
+
99
+ A controller with business logic, conditional branching, direct mailer calls, and inline data transformations:
100
+
101
+ ```ruby
102
+ class OrdersController < ApplicationController
103
+ def create
104
+ @order = Order.new(order_params)
105
+ @order.user = current_user
106
+
107
+ # Business logic in controller
108
+ @order.line_items.each do |item|
109
+ product = Product.find(item.product_id)
110
+ if product.stock < item.quantity
111
+ flash[:alert] = "#{product.name} only has #{product.stock} left"
112
+ render :new and return
113
+ end
114
+ item.unit_price = product.price
115
+ item.total = product.price * item.quantity
116
+ end
117
+
118
+ @order.subtotal = @order.line_items.sum(&:total)
119
+ @order.tax = @order.subtotal * 0.08
120
+ @order.total = @order.subtotal + @order.tax
121
+
122
+ if current_user.loyalty_points >= 100
123
+ discount = (@order.total * 0.1).round(2)
124
+ @order.discount = discount
125
+ @order.total -= discount
126
+ current_user.update(loyalty_points: current_user.loyalty_points - 100)
127
+ end
128
+
129
+ if @order.save
130
+ @order.line_items.each do |item|
131
+ product = Product.find(item.product_id)
132
+ product.update!(stock: product.stock - item.quantity)
133
+ end
134
+ OrderMailer.confirmation(@order).deliver_later
135
+ AdminMailer.new_order(@order).deliver_later if @order.total > 500
136
+ redirect_to @order, notice: "Order placed!"
137
+ else
138
+ render :new
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
144
+ ## Why This Is Bad
145
+
146
+ - **50+ lines for one action.** A developer has to read the entire method to understand what creating an order involves. The HTTP concerns (params, render, redirect) are buried among price calculations and stock updates.
147
+ - **Untestable in isolation.** To test order creation you must make HTTP requests, set up products with stock levels, loyalty points, and assert mailer deliveries — all in one test.
148
+ - **Logic is trapped.** When you need to create orders from an API endpoint, a Sidekiq job, or the console, you can't. The logic is locked inside an HTTP controller action.
149
+ - **Multiple responsibilities.** This action validates stock, calculates prices, applies discounts, manages loyalty points, updates inventory, and sends emails. Changing any one of these risks breaking the others.
150
+ - **Missing status codes.** The failure case renders `:new` without `status: :unprocessable_entity`, which breaks Turbo and returns 200 on validation failure.
151
+
152
+ ## When To Apply
153
+
154
+ Always. Every Rails controller should follow skinny principles. The question isn't "should this controller be skinny?" — it's "where does the extracted logic go?"
155
+
156
+ - Simple CRUD (save one record, no side effects) → logic stays in the model
157
+ - Complex creation/updates (multiple models, side effects) → service object
158
+ - Complex validations (virtual attributes, multi-model validation) → form object
159
+ - Complex queries (reporting, search, filtering) → query object
160
+ - Shared controller behavior (auth, pagination, error handling) → controller concern
161
+
162
+ ## When NOT To Apply
163
+
164
+ There is no case where a fat controller is the right choice. However, there are cases where extracting logic is premature:
165
+
166
+ - A 3-line create action that saves a record and redirects does NOT need a service object. The controller is already skinny.
167
+ - Simple `before_action` callbacks for setting records are fine in the controller. They don't need extraction.
168
+ - Standard `params.require().permit()` belongs in the controller, not in a separate class (unless the params logic itself is complex — then use a form object).
169
+
170
+ ## Edge Cases
171
+
172
+ **The action is 8 lines but all the logic is param handling:**
173
+ That's a sign you need a form object, not a service object. If you're transforming, nesting, or conditionally including params, extract to a form object.
174
+
175
+ **You need to return different formats (HTML, JSON, CSV):**
176
+ Use `respond_to` blocks in the controller — format selection IS an HTTP concern. But keep the data preparation in a service or query object.
177
+
178
+ ```ruby
179
+ def index
180
+ @orders = Orders::SearchQuery.call(search_params)
181
+ respond_to do |format|
182
+ format.html
183
+ format.json { render json: OrderSerializer.new(@orders) }
184
+ format.csv { send_data Orders::CsvExporter.call(@orders), filename: "orders.csv" }
185
+ end
186
+ end
187
+ ```
188
+
189
+ **The team uses `before_action` for everything:**
190
+ Before actions are good for record loading and auth checks. They're bad for business logic. If a before action does more than `set_X` or `authorize_X`, it's hiding complexity in the wrong place.
@@ -0,0 +1,201 @@
1
+ # Rails: Engines
2
+
3
+ ## Pattern
4
+
5
+ A Rails engine is a miniature Rails application that can be mounted inside a host app. Engines package controllers, models, views, routes, and assets into a self-contained, reusable component. Use them for features that are isolated from the host app's domain — admin panels, dev tools, billing dashboards, and embeddable widgets.
6
+
7
+ ```ruby
8
+ # Generate a mountable engine
9
+ # rails plugin new rubyn --mountable
10
+
11
+ # lib/rubyn/engine.rb
12
+ module Rubyn
13
+ class Engine < ::Rails::Engine
14
+ isolate_namespace Rubyn
15
+
16
+ # Engine-specific configuration
17
+ config.generators do |g|
18
+ g.test_framework :rspec
19
+ g.assets false # Engine manages its own assets
20
+ end
21
+
22
+ # Initializers run when the host app boots
23
+ initializer "rubyn.assets" do |app|
24
+ app.config.assets.precompile += %w[rubyn/application.css rubyn/application.js] if app.config.respond_to?(:assets)
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ```ruby
31
+ # Engine routes — completely isolated from host app
32
+ # config/routes.rb (inside the engine)
33
+ Rubyn::Engine.routes.draw do
34
+ root to: "dashboard#show"
35
+
36
+ resources :files, only: [:index, :show]
37
+ resource :agent, only: [:show, :create]
38
+
39
+ namespace :ai do
40
+ post :refactor
41
+ post :review
42
+ post :spec
43
+ end
44
+
45
+ resource :settings, only: [:show, :update]
46
+ end
47
+ ```
48
+
49
+ ```ruby
50
+ # Host app mounts the engine
51
+ # config/routes.rb (host app)
52
+ Rails.application.routes.draw do
53
+ mount Rubyn::Engine => "/rubyn" if Rails.env.development?
54
+
55
+ # Host app's own routes
56
+ resources :orders
57
+ end
58
+ ```
59
+
60
+ ### Engine Controllers
61
+
62
+ ```ruby
63
+ # app/controllers/rubyn/application_controller.rb
64
+ module Rubyn
65
+ class ApplicationController < ActionController::Base
66
+ layout "rubyn/application" # Engine's own layout
67
+
68
+ before_action :verify_development_environment
69
+
70
+ private
71
+
72
+ def verify_development_environment
73
+ head :forbidden unless Rails.env.development?
74
+ end
75
+
76
+ # Engine reads credentials from the user's local config
77
+ def rubyn_api_key
78
+ @rubyn_api_key ||= Rubyn::Config.api_key
79
+ end
80
+ end
81
+ end
82
+
83
+ # app/controllers/rubyn/dashboard_controller.rb
84
+ module Rubyn
85
+ class DashboardController < ApplicationController
86
+ def show
87
+ @project_info = Rubyn::ProjectScanner.scan(Rails.root)
88
+ @credit_balance = Rubyn::ApiClient.new(rubyn_api_key).balance
89
+ @recent_activity = Rubyn::ApiClient.new(rubyn_api_key).recent_interactions(limit: 10)
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### Engine Views (Self-Contained)
96
+
97
+ ```erb
98
+ <%# app/views/layouts/rubyn/application.html.erb %>
99
+ <%# Engine has its own layout — doesn't depend on host app's layout %>
100
+ <!DOCTYPE html>
101
+ <html>
102
+ <head>
103
+ <title>Rubyn</title>
104
+ <%= csrf_meta_tags %>
105
+ <%= stylesheet_link_tag "rubyn/application", media: "all" %>
106
+ </head>
107
+ <body class="rubyn-app">
108
+ <nav class="rubyn-nav">
109
+ <%= link_to "Dashboard", rubyn.root_path %>
110
+ <%= link_to "Files", rubyn.files_path %>
111
+ <%= link_to "Agent", rubyn.agent_path %>
112
+ <%= link_to "Settings", rubyn.settings_path %>
113
+ </nav>
114
+
115
+ <main>
116
+ <%= yield %>
117
+ </main>
118
+
119
+ <%= javascript_include_tag "rubyn/application" %>
120
+ </body>
121
+ </html>
122
+ ```
123
+
124
+ ### Namespace Isolation
125
+
126
+ ```ruby
127
+ # isolate_namespace ensures the engine doesn't pollute the host app
128
+
129
+ # Engine model — lives in rubyn_ prefixed tables
130
+ module Rubyn
131
+ class Interaction < ApplicationRecord
132
+ # Table: rubyn_interactions (not interactions)
133
+ end
134
+ end
135
+
136
+ # Engine routes are namespaced
137
+ rubyn.root_path # => "/rubyn"
138
+ rubyn.files_path # => "/rubyn/files"
139
+ main_app.orders_path # => "/orders" (host app routes)
140
+
141
+ # In engine views, explicitly reference host vs engine routes:
142
+ <%= link_to "Back to app", main_app.root_path %>
143
+ <%= link_to "Dashboard", rubyn.root_path %>
144
+ ```
145
+
146
+ ## Why This Is Good
147
+
148
+ - **Complete isolation.** The engine has its own namespace, routes, views, assets, and optionally its own database tables. It can't accidentally conflict with the host app's controllers or styles.
149
+ - **Mountable with one line.** `mount Rubyn::Engine => "/rubyn"` — the host app adds one line and gets a full-featured dev dashboard.
150
+ - **Development-only by default.** `if Rails.env.development?` ensures the engine never accidentally runs in production.
151
+ - **Self-contained assets.** The engine ships its own CSS and JavaScript. No dependency on the host app's Tailwind config, asset pipeline, or build tools.
152
+ - **Shareable across projects.** Package the engine as a gem, install it in any Rails project, mount it — instant dev tools.
153
+
154
+ ## When To Apply
155
+
156
+ - **Dev tools** — dashboards, profilers, debug panels, AI coding assistants. Features that help developers but shouldn't exist in production.
157
+ - **Admin panels** — self-contained admin interfaces with their own auth, layout, and styles.
158
+ - **Shared features across apps** — authentication, billing, notifications, CMS. Build once, mount in multiple apps.
159
+ - **Rubyn itself** — the mountable web UI is an engine inside the `rubyn` gem.
160
+
161
+ ## When NOT To Apply
162
+
163
+ - **Feature that's tightly coupled to the host app's domain.** If the feature needs to share models, validations, and business logic with the host app, it's not a good engine candidate — it's just part of the app.
164
+ - **Simple shared code.** A few utility methods shared across apps should be a gem with modules, not an engine with controllers and views.
165
+ - **One-off features.** Don't engine-ify something used in only one app. Engines add architectural overhead.
166
+
167
+ ## Edge Cases
168
+
169
+ **Engine accessing host app's models:**
170
+ ```ruby
171
+ # The engine can reference host app models if they exist
172
+ class Rubyn::DashboardController < Rubyn::ApplicationController
173
+ def show
174
+ # Access host app's models — works but creates coupling
175
+ @user_count = ::User.count if defined?(::User)
176
+ end
177
+ end
178
+ ```
179
+ Minimize this — the engine should work without knowing the host app's models.
180
+
181
+ **Testing engines:**
182
+ ```ruby
183
+ # The engine includes a dummy Rails app for testing
184
+ # test/dummy/ contains a minimal Rails app that mounts the engine
185
+ # spec/dummy/ for RSpec
186
+
187
+ # spec/requests/rubyn/dashboard_spec.rb
188
+ RSpec.describe "Rubyn::Dashboard", type: :request do
189
+ it "shows the dashboard" do
190
+ get rubyn.root_path
191
+ expect(response).to have_http_status(:ok)
192
+ end
193
+ end
194
+ ```
195
+
196
+ **Engine migrations:**
197
+ ```bash
198
+ # Copy engine migrations to host app
199
+ rails rubyn:install:migrations
200
+ rails db:migrate
201
+ ```