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,158 @@
1
+ # Design Pattern: Builder
2
+
3
+ ## Pattern
4
+
5
+ Construct complex objects step by step. The Builder pattern lets you produce different representations of an object using the same construction process. In Ruby, builders are often implemented as chainable method calls or configuration blocks.
6
+
7
+ ```ruby
8
+ # Builder with chainable methods — idiomatic Ruby
9
+ class PromptBuilder
10
+ def initialize
11
+ @system_parts = []
12
+ @messages = []
13
+ @model = "claude-haiku-4-5-20251001"
14
+ @max_tokens = 4096
15
+ @temperature = 0.0
16
+ end
17
+
18
+ def system(content)
19
+ @system_parts << content
20
+ self
21
+ end
22
+
23
+ def best_practice(document)
24
+ @system_parts << "## Best Practice: #{document.title}\n\n#{document.content}"
25
+ self
26
+ end
27
+
28
+ def codebase_context(embeddings)
29
+ context = embeddings.map { |e| "# #{e.file_path}\n```ruby\n#{e.chunk_content}\n```" }.join("\n\n")
30
+ @system_parts << "## Relevant Codebase Context\n\n#{context}"
31
+ self
32
+ end
33
+
34
+ def user(content)
35
+ @messages << { role: "user", content: content }
36
+ self
37
+ end
38
+
39
+ def assistant(content)
40
+ @messages << { role: "assistant", content: content }
41
+ self
42
+ end
43
+
44
+ def model(name)
45
+ @model = name
46
+ self
47
+ end
48
+
49
+ def max_tokens(n)
50
+ @max_tokens = n
51
+ self
52
+ end
53
+
54
+ def temperature(t)
55
+ @temperature = t
56
+ self
57
+ end
58
+
59
+ def build
60
+ {
61
+ model: @model,
62
+ max_tokens: @max_tokens,
63
+ temperature: @temperature,
64
+ system: @system_parts.join("\n\n---\n\n"),
65
+ messages: @messages
66
+ }
67
+ end
68
+ end
69
+
70
+ # Usage — reads like a recipe
71
+ prompt = PromptBuilder.new
72
+ .system("You are Rubyn, an expert Ruby and Rails coding assistant.")
73
+ .best_practice(service_objects_doc)
74
+ .best_practice(callbacks_doc)
75
+ .codebase_context(relevant_embeddings)
76
+ .user("Refactor this controller action into a service object:\n\n```ruby\n#{code}\n```")
77
+ .model("claude-haiku-4-5-20251001")
78
+ .max_tokens(4096)
79
+ .build
80
+ ```
81
+
82
+ Builder with block configuration — Ruby convention:
83
+
84
+ ```ruby
85
+ class QueryBuilder
86
+ attr_reader :scope
87
+
88
+ def initialize(base_scope)
89
+ @scope = base_scope
90
+ end
91
+
92
+ def self.build(base_scope, &block)
93
+ builder = new(base_scope)
94
+ builder.instance_eval(&block) if block
95
+ builder.scope
96
+ end
97
+
98
+ def where(**conditions)
99
+ @scope = @scope.where(conditions)
100
+ end
101
+
102
+ def search(query)
103
+ return unless query.present?
104
+ @scope = @scope.where("name ILIKE ?", "%#{query}%")
105
+ end
106
+
107
+ def status(value)
108
+ return unless value.present?
109
+ @scope = @scope.where(status: value)
110
+ end
111
+
112
+ def date_range(from:, to:)
113
+ @scope = @scope.where(created_at: from..to) if from && to
114
+ end
115
+
116
+ def sort_by(column, direction = :asc)
117
+ @scope = @scope.order(column => direction)
118
+ end
119
+
120
+ def paginate(page:, per: 25)
121
+ @scope = @scope.page(page).per(per)
122
+ end
123
+ end
124
+
125
+ # Usage with block
126
+ orders = QueryBuilder.build(current_user.orders) do
127
+ status params[:status]
128
+ search params[:q]
129
+ date_range from: params[:from], to: params[:to]
130
+ sort_by :created_at, :desc
131
+ paginate page: params[:page]
132
+ end
133
+ ```
134
+
135
+ ## Why This Is Good
136
+
137
+ - **Step-by-step construction.** Complex objects are built incrementally. Each step is named and self-documenting. The final `build` call assembles everything.
138
+ - **Optional steps.** Not every prompt needs best practices or codebase context. The builder doesn't care which steps are called or in what order.
139
+ - **Chainable API is readable.** `.system(...).best_practice(...).user(...)` reads as a sequence of construction steps. It's clearer than a constructor with 8 keyword arguments.
140
+ - **Separates construction from representation.** The same builder process can produce different outputs — a hash for the API, a string for logging, an object for testing.
141
+ - **Block form is idiomatic Ruby.** `QueryBuilder.build(scope) { status "active" }` follows Ruby conventions (like `Faraday.new { |f| f.adapter :net_http }`).
142
+
143
+ ## When To Apply
144
+
145
+ - **Objects with many optional parts.** An API request with optional system prompt, codebase context, conversation history, model selection, and temperature.
146
+ - **Objects constructed in different configurations.** A query that sometimes has filters, sometimes has sorting, sometimes has pagination — but never all of them.
147
+ - **When a constructor has 5+ parameters.** The builder replaces a long argument list with named, chainable steps.
148
+ - **Testing.** Builders make it easy to create test fixtures with specific configurations without specifying every field.
149
+
150
+ ## When NOT To Apply
151
+
152
+ - **Simple objects with 2-3 required fields.** `Order.new(user: user, total: 100)` doesn't need a builder.
153
+ - **Objects that are always constructed the same way.** If every construction uses the same steps, a factory method is simpler.
154
+ - **Don't create a builder just for one call site.** Builders shine when used from multiple places with different configurations.
155
+
156
+ ## Rails Examples
157
+
158
+ Rails uses the builder pattern extensively — `Arel` query building, `ActionMailer` message construction, `ActiveStorage` attachment configuration. Follow the same pattern for your domain objects.
@@ -0,0 +1,126 @@
1
+ # Design Pattern: Command
2
+
3
+ ## Pattern
4
+
5
+ Encapsulate a request as an object, allowing you to parameterize clients with different requests, queue requests, log them, and support undo operations. In Ruby/Rails, service objects are already a form of the Command pattern — each one encapsulates a single operation.
6
+
7
+ ```ruby
8
+ # Commands as objects — queueable, loggable, undoable
9
+ class Commands::Base
10
+ attr_reader :executed_at, :result
11
+
12
+ def execute
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def undo
17
+ raise NotImplementedError, "#{self.class} does not support undo"
18
+ end
19
+
20
+ def description
21
+ raise NotImplementedError
22
+ end
23
+ end
24
+
25
+ class Commands::ChangeOrderStatus < Commands::Base
26
+ def initialize(order, new_status, actor:)
27
+ @order = order
28
+ @new_status = new_status
29
+ @actor = actor
30
+ @previous_status = order.status
31
+ end
32
+
33
+ def execute
34
+ @order.update!(status: @new_status)
35
+ @executed_at = Time.current
36
+ AuditLog.record(actor: @actor, action: description, target: @order)
37
+ @result = :success
38
+ end
39
+
40
+ def undo
41
+ @order.update!(status: @previous_status)
42
+ AuditLog.record(actor: @actor, action: "Undo: #{description}", target: @order)
43
+ end
44
+
45
+ def description
46
+ "Changed order #{@order.reference} from #{@previous_status} to #{@new_status}"
47
+ end
48
+ end
49
+
50
+ class Commands::ApplyDiscount < Commands::Base
51
+ def initialize(order, discount_code, actor:)
52
+ @order = order
53
+ @discount_code = discount_code
54
+ @actor = actor
55
+ @previous_discount = order.discount_amount
56
+ end
57
+
58
+ def execute
59
+ discount = Discount.active.find_by!(code: @discount_code)
60
+ amount = discount.calculate(@order.subtotal)
61
+ @order.update!(discount_amount: amount, discount_code: @discount_code)
62
+ @executed_at = Time.current
63
+ @result = :success
64
+ end
65
+
66
+ def undo
67
+ @order.update!(discount_amount: @previous_discount, discount_code: nil)
68
+ end
69
+
70
+ def description
71
+ "Applied discount #{@discount_code} to order #{@order.reference}"
72
+ end
73
+ end
74
+
75
+ # Command history for undo support
76
+ class CommandHistory
77
+ def initialize
78
+ @history = []
79
+ end
80
+
81
+ def execute(command)
82
+ command.execute
83
+ @history.push(command)
84
+ command
85
+ end
86
+
87
+ def undo_last
88
+ command = @history.pop
89
+ return unless command
90
+ command.undo
91
+ command
92
+ end
93
+
94
+ def log
95
+ @history.map { |cmd| "#{cmd.executed_at}: #{cmd.description}" }
96
+ end
97
+ end
98
+
99
+ # Usage
100
+ history = CommandHistory.new
101
+ history.execute(Commands::ChangeOrderStatus.new(order, "confirmed", actor: admin))
102
+ history.execute(Commands::ApplyDiscount.new(order, "SAVE10", actor: admin))
103
+
104
+ # Undo last action
105
+ history.undo_last # Reverses the discount
106
+ ```
107
+
108
+ ## Why This Is Good
109
+
110
+ - **Operations are first-class objects.** Each command can be queued, logged, serialized, and undone. You can't do this with bare method calls.
111
+ - **Audit trail is built in.** Every command has a `description` and `executed_at`. The history is an automatic audit log.
112
+ - **Undo support.** Each command stores the state needed to reverse itself. Admin actions, bulk operations, and user-facing "undo" features are straightforward.
113
+ - **Deferred execution.** Commands can be serialized and executed later — in a background job, after approval, or in a batch.
114
+
115
+ ## When To Apply
116
+
117
+ - **Admin actions that need audit trails.** Status changes, refunds, account modifications — wrap each in a command that logs who did what.
118
+ - **User-facing undo.** "Undo archive", "undo delete", "undo status change" — commands store previous state.
119
+ - **Batch operations.** Collect multiple commands, validate them all, then execute as a group.
120
+ - **Background job payloads.** Serialize a command and enqueue it. The job deserializes and executes.
121
+
122
+ ## When NOT To Apply
123
+
124
+ - **Simple CRUD without undo or audit.** A standard `Order.create!(params)` doesn't need a Command wrapper.
125
+ - **Your existing service objects already work.** If service objects handle your use case without undo or queueing needs, don't add Command on top.
126
+ - **Fire-and-forget operations.** If you never need to undo or replay the action, a plain service object is simpler.
@@ -0,0 +1,147 @@
1
+ # Design Pattern: Composite
2
+
3
+ ## Pattern
4
+
5
+ Compose objects into tree structures to represent part-whole hierarchies. The Composite pattern lets clients treat individual objects and compositions of objects uniformly — the same interface for a single item and a group of items.
6
+
7
+ ```ruby
8
+ # Permission system — individual permissions and permission groups share the same interface
9
+
10
+ class Permission
11
+ attr_reader :name
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ def grants?(action)
18
+ name == action
19
+ end
20
+
21
+ def all_permissions
22
+ [name]
23
+ end
24
+
25
+ def to_s
26
+ name
27
+ end
28
+ end
29
+
30
+ class PermissionGroup
31
+ attr_reader :name
32
+
33
+ def initialize(name)
34
+ @name = name
35
+ @children = []
36
+ end
37
+
38
+ def add(permission)
39
+ @children << permission
40
+ self
41
+ end
42
+
43
+ def grants?(action)
44
+ @children.any? { |child| child.grants?(action) }
45
+ end
46
+
47
+ def all_permissions
48
+ @children.flat_map(&:all_permissions)
49
+ end
50
+
51
+ def to_s
52
+ "#{name}: [#{@children.map(&:to_s).join(', ')}]"
53
+ end
54
+ end
55
+
56
+ # Build a permission tree
57
+ read_code = Permission.new("code:read")
58
+ write_code = Permission.new("code:write")
59
+ delete_code = Permission.new("code:delete")
60
+
61
+ code_admin = PermissionGroup.new("code_admin")
62
+ .add(read_code)
63
+ .add(write_code)
64
+ .add(delete_code)
65
+
66
+ read_billing = Permission.new("billing:read")
67
+ manage_billing = Permission.new("billing:manage")
68
+
69
+ billing_admin = PermissionGroup.new("billing_admin")
70
+ .add(read_billing)
71
+ .add(manage_billing)
72
+
73
+ super_admin = PermissionGroup.new("super_admin")
74
+ .add(code_admin) # Group containing group
75
+ .add(billing_admin) # Group containing group
76
+
77
+ # Uniform interface — works the same for single permissions and groups
78
+ read_code.grants?("code:read") # true
79
+ code_admin.grants?("code:read") # true
80
+ super_admin.grants?("billing:manage") # true — traverses the tree
81
+ super_admin.all_permissions
82
+ # => ["code:read", "code:write", "code:delete", "billing:read", "billing:manage"]
83
+ ```
84
+
85
+ Rails-practical example — pricing rules:
86
+
87
+ ```ruby
88
+ # Single rule and rule groups share the same interface
89
+ class Pricing::FlatDiscount
90
+ def initialize(amount)
91
+ @amount = amount
92
+ end
93
+
94
+ def apply(price)
95
+ [price - @amount, 0].max
96
+ end
97
+ end
98
+
99
+ class Pricing::PercentDiscount
100
+ def initialize(percent)
101
+ @percent = percent
102
+ end
103
+
104
+ def apply(price)
105
+ price * (1 - @percent / 100.0)
106
+ end
107
+ end
108
+
109
+ class Pricing::DiscountChain
110
+ def initialize
111
+ @discounts = []
112
+ end
113
+
114
+ def add(discount)
115
+ @discounts << discount
116
+ self
117
+ end
118
+
119
+ def apply(price)
120
+ @discounts.reduce(price) { |p, discount| discount.apply(p) }
121
+ end
122
+ end
123
+
124
+ # Compose discounts
125
+ holiday_deal = Pricing::DiscountChain.new
126
+ .add(Pricing::PercentDiscount.new(10)) # 10% off first
127
+ .add(Pricing::FlatDiscount.new(5_00)) # Then $5 off
128
+
129
+ final_price = holiday_deal.apply(100_00) # $100 → $90 → $85
130
+ ```
131
+
132
+ ## Why This Is Good
133
+
134
+ - **Uniform interface.** `grants?("code:read")` works on a single permission, a group, or a tree of groups. The caller never checks types.
135
+ - **Recursive composition.** Groups can contain other groups. `super_admin` contains `code_admin` which contains individual permissions. Any depth works.
136
+ - **Easy to extend.** New permission types (time-limited, IP-restricted) just implement `grants?` and `all_permissions`. They plug into any group.
137
+
138
+ ## When To Apply
139
+
140
+ - **Tree structures** — menus, categories, org charts, file systems, permission hierarchies.
141
+ - **Part-whole relationships** — a single discount and a chain of discounts, a single validator and a validator pipeline.
142
+ - **When clients need to treat single items and collections identically.**
143
+
144
+ ## When NOT To Apply
145
+
146
+ - **Flat collections.** If items don't nest, use a simple array. Don't build a Composite for a list.
147
+ - **When the leaf and composite have very different interfaces.** If a single permission and a permission group need fundamentally different methods, Composite adds forced uniformity.
@@ -0,0 +1,204 @@
1
+ # Design Pattern: Decorator
2
+
3
+ ## Pattern
4
+
5
+ Attach additional behavior to an object dynamically by wrapping it in a decorator object. The decorator forwards method calls to the wrapped object and adds behavior before, after, or around the delegation. In Ruby, decorators are often implemented with `SimpleDelegator` or `method_missing`, but explicit delegation is clearest.
6
+
7
+ ```ruby
8
+ # Base class — the object to be decorated
9
+ class Ai::CompletionClient
10
+ def complete(messages, model:, max_tokens:)
11
+ response = Anthropic::Client.new.messages.create(
12
+ model: model,
13
+ max_tokens: max_tokens,
14
+ messages: messages
15
+ )
16
+ CompletionResult.new(
17
+ content: response.content.first.text,
18
+ input_tokens: response.usage.input_tokens,
19
+ output_tokens: response.usage.output_tokens
20
+ )
21
+ end
22
+ end
23
+
24
+ # Decorator: adds logging around the real call
25
+ class Ai::LoggingDecorator
26
+ def initialize(client)
27
+ @client = client
28
+ end
29
+
30
+ def complete(messages, model:, max_tokens:)
31
+ Rails.logger.info("[AI] Requesting #{model} with #{messages.length} messages")
32
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+
34
+ result = @client.complete(messages, model: model, max_tokens: max_tokens)
35
+
36
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
37
+ Rails.logger.info("[AI] Completed in #{elapsed.round(2)}s — #{result.input_tokens}in/#{result.output_tokens}out")
38
+
39
+ result
40
+ end
41
+ end
42
+
43
+ # Decorator: adds caching
44
+ class Ai::CachingDecorator
45
+ def initialize(client, cache: Rails.cache, ttl: 1.hour)
46
+ @client = client
47
+ @cache = cache
48
+ @ttl = ttl
49
+ end
50
+
51
+ def complete(messages, model:, max_tokens:)
52
+ cache_key = "ai:#{Digest::SHA256.hexdigest(messages.to_json)}:#{model}"
53
+
54
+ @cache.fetch(cache_key, expires_in: @ttl) do
55
+ @client.complete(messages, model: model, max_tokens: max_tokens)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Decorator: adds retry logic
61
+ class Ai::RetryDecorator
62
+ def initialize(client, max_retries: 3)
63
+ @client = client
64
+ @max_retries = max_retries
65
+ end
66
+
67
+ def complete(messages, model:, max_tokens:)
68
+ retries = 0
69
+ begin
70
+ @client.complete(messages, model: model, max_tokens: max_tokens)
71
+ rescue Faraday::TimeoutError, Faraday::ServerError => e
72
+ retries += 1
73
+ raise if retries > @max_retries
74
+ sleep(2**retries + rand(0.0..0.5))
75
+ retry
76
+ end
77
+ end
78
+ end
79
+
80
+ # Compose decorators — each wraps the previous one
81
+ client = Ai::CompletionClient.new
82
+ client = Ai::RetryDecorator.new(client)
83
+ client = Ai::CachingDecorator.new(client)
84
+ client = Ai::LoggingDecorator.new(client)
85
+
86
+ # The caller sees ONE object with ONE interface
87
+ result = client.complete(messages, model: "claude-haiku-4-5-20251001", max_tokens: 4096)
88
+ # Logs → checks cache → retries on failure → calls Anthropic
89
+ ```
90
+
91
+ Using `SimpleDelegator` for view decorators (presenters):
92
+
93
+ ```ruby
94
+ class OrderPresenter < SimpleDelegator
95
+ def formatted_total
96
+ "$#{format('%.2f', total)}"
97
+ end
98
+
99
+ def status_badge
100
+ case status
101
+ when "pending" then '<span class="badge bg-warning">Pending</span>'
102
+ when "shipped" then '<span class="badge bg-info">Shipped</span>'
103
+ when "delivered" then '<span class="badge bg-success">Delivered</span>'
104
+ else '<span class="badge bg-secondary">Unknown</span>'
105
+ end.html_safe
106
+ end
107
+
108
+ def created_at_formatted
109
+ created_at.strftime("%B %d, %Y at %I:%M %p")
110
+ end
111
+ end
112
+
113
+ # Usage in controller
114
+ @order = OrderPresenter.new(Order.find(params[:id]))
115
+
116
+ # In the view, all Order methods work plus the presenter methods
117
+ <%= @order.formatted_total %>
118
+ <%= @order.status_badge %>
119
+ <%= @order.user.name %> <!-- delegated to the real Order -->
120
+ ```
121
+
122
+ ## Why This Is Good
123
+
124
+ - **Composable behaviors.** Logging, caching, and retry are separate concerns, each in its own class. You compose them like LEGO — add or remove as needed.
125
+ - **Same interface throughout.** Every decorator responds to `complete(messages, model:, max_tokens:)`. The caller doesn't know or care how many decorators are stacked.
126
+ - **Open/Closed compliant.** Adding rate limiting means writing a `RateLimitDecorator` — not modifying the client, the logger, or the cache.
127
+ - **Testable in isolation.** Test `RetryDecorator` by wrapping a fake client that fails twice then succeeds. No real HTTP, no logging, no caching involved.
128
+ - **Presenters keep views clean.** `@order.formatted_total` is cleaner than `number_to_currency(@order.total)` scattered across 10 views.
129
+
130
+ ## Anti-Pattern
131
+
132
+ Putting all cross-cutting concerns inside the base class:
133
+
134
+ ```ruby
135
+ class Ai::CompletionClient
136
+ def complete(messages, model:, max_tokens:)
137
+ cache_key = "ai:#{Digest::SHA256.hexdigest(messages.to_json)}"
138
+ cached = Rails.cache.read(cache_key)
139
+ return cached if cached
140
+
141
+ Rails.logger.info("[AI] Requesting #{model}")
142
+ start = Time.now
143
+
144
+ retries = 0
145
+ begin
146
+ response = Anthropic::Client.new.messages.create(
147
+ model: model, max_tokens: max_tokens, messages: messages
148
+ )
149
+ rescue Faraday::TimeoutError
150
+ retries += 1
151
+ retry if retries <= 3
152
+ raise
153
+ end
154
+
155
+ elapsed = Time.now - start
156
+ Rails.logger.info("[AI] Completed in #{elapsed}s")
157
+
158
+ result = CompletionResult.new(content: response.content.first.text)
159
+ Rails.cache.write(cache_key, result, expires_in: 1.hour)
160
+ result
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Why This Is Bad
166
+
167
+ - **One 30-line method with 4 responsibilities.** API call, logging, caching, and retry are tangled together. Modifying retry logic means reading through cache and logging code.
168
+ - **Can't disable caching for tests.** The cache is hardcoded. Tests either hit the cache (stale results) or need `Rails.cache.clear` before every test.
169
+ - **Can't reuse retry logic.** If the embedding client also needs retry, you duplicate the retry block. With a decorator, `RetryDecorator.new(embedding_client)` reuses it.
170
+
171
+ ## When To Apply
172
+
173
+ - **Cross-cutting concerns** — logging, caching, retry, rate limiting, metrics, authentication wrapping. Each is a decorator.
174
+ - **View presentation logic** — formatting dates, currencies, status badges, display names. Use `SimpleDelegator` presenters.
175
+ - **Feature toggles** — a decorator that conditionally enables new behavior while forwarding to the old behavior by default.
176
+ - **API response transformation** — a decorator that normalizes different API response formats into a consistent internal structure.
177
+
178
+ ## When NOT To Apply
179
+
180
+ - **One behavior that won't be reused.** If only the AI client needs retry logic and nothing else ever will, putting retry inline is simpler than a decorator class.
181
+ - **Deep stacks obscure behavior.** If you stack 7 decorators, debugging which one modified the response is difficult. Keep stacks to 3-4 max.
182
+ - **Don't decorate ActiveRecord models for persistence logic.** Use service objects. Decorators are for presentation and cross-cutting concerns, not business logic.
183
+
184
+ ## Edge Cases
185
+
186
+ **`Module#prepend` as an inline decorator:**
187
+
188
+ ```ruby
189
+ module Logging
190
+ def complete(messages, model:, max_tokens:)
191
+ Rails.logger.info("[AI] Requesting #{model}")
192
+ result = super
193
+ Rails.logger.info("[AI] Done: #{result.input_tokens} tokens")
194
+ result
195
+ end
196
+ end
197
+
198
+ Ai::CompletionClient.prepend(Logging)
199
+ ```
200
+
201
+ This is Ruby's most concise decorator pattern but less flexible — it modifies the class globally rather than per-instance.
202
+
203
+ **Draper gem for view decorators:**
204
+ If the team uses Draper, follow its conventions. Otherwise, `SimpleDelegator` is lighter and framework-free.