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,177 @@
1
+ # Design Pattern: Observer
2
+
3
+ ## Pattern
4
+
5
+ Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. In Rails, this replaces scattered callbacks with explicit, decoupled event handling.
6
+
7
+ ```ruby
8
+ # Simple Ruby observer using ActiveSupport::Notifications (Rails built-in)
9
+
10
+ # PUBLISHER: fires events after key actions
11
+ class Orders::CreateService
12
+ def call(params, user)
13
+ order = user.orders.create!(params)
14
+
15
+ # Publish event — doesn't know or care who's listening
16
+ ActiveSupport::Notifications.instrument("order.created", order: order)
17
+
18
+ Result.new(success: true, order: order)
19
+ rescue ActiveRecord::RecordInvalid => e
20
+ Result.new(success: false, order: e.record)
21
+ end
22
+ end
23
+
24
+ # SUBSCRIBERS: each handles one concern, registered independently
25
+
26
+ # config/initializers/event_subscribers.rb
27
+ ActiveSupport::Notifications.subscribe("order.created") do |*, payload|
28
+ order = payload[:order]
29
+ OrderMailer.confirmation(order).deliver_later
30
+ end
31
+
32
+ ActiveSupport::Notifications.subscribe("order.created") do |*, payload|
33
+ order = payload[:order]
34
+ WarehouseNotificationJob.perform_later(order.id)
35
+ end
36
+
37
+ ActiveSupport::Notifications.subscribe("order.created") do |*, payload|
38
+ order = payload[:order]
39
+ Analytics.track("order_created", order_id: order.id, total: order.total)
40
+ end
41
+ ```
42
+
43
+ Custom event system for more structure:
44
+
45
+ ```ruby
46
+ # app/events/event_bus.rb
47
+ module EventBus
48
+ SUBSCRIBERS = Hash.new { |h, k| h[k] = [] }
49
+
50
+ def self.subscribe(event_name, handler)
51
+ SUBSCRIBERS[event_name] << handler
52
+ end
53
+
54
+ def self.publish(event_name, **payload)
55
+ SUBSCRIBERS[event_name].each do |handler|
56
+ handler.call(**payload)
57
+ rescue StandardError => e
58
+ Rails.logger.error("EventBus: #{handler} failed for #{event_name}: #{e.message}")
59
+ # Don't let one subscriber failure block others
60
+ end
61
+ end
62
+ end
63
+
64
+ # Subscriber classes — focused, testable
65
+ class OrderCreatedHandlers::SendConfirmation
66
+ def self.call(order:)
67
+ OrderMailer.confirmation(order).deliver_later
68
+ end
69
+ end
70
+
71
+ class OrderCreatedHandlers::NotifyWarehouse
72
+ def self.call(order:)
73
+ WarehouseNotificationJob.perform_later(order.id)
74
+ end
75
+ end
76
+
77
+ class OrderCreatedHandlers::TrackAnalytics
78
+ def self.call(order:)
79
+ Analytics.track("order_created", order_id: order.id, total: order.total)
80
+ end
81
+ end
82
+
83
+ # Registration
84
+ EventBus.subscribe("order.created", OrderCreatedHandlers::SendConfirmation)
85
+ EventBus.subscribe("order.created", OrderCreatedHandlers::NotifyWarehouse)
86
+ EventBus.subscribe("order.created", OrderCreatedHandlers::TrackAnalytics)
87
+
88
+ # Publisher — fires and forgets
89
+ class Orders::CreateService
90
+ def call(params, user)
91
+ order = user.orders.create!(params)
92
+ EventBus.publish("order.created", order: order)
93
+ Result.new(success: true, order: order)
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Why This Is Good
99
+
100
+ - **Publisher doesn't know its subscribers.** `CreateService` publishes "order.created" and moves on. It doesn't import, reference, or depend on mailers, warehouses, or analytics.
101
+ - **Adding new reactions doesn't modify existing code.** Sending a Slack notification on order creation? Add one subscriber. The publisher, the mailer subscriber, and the warehouse subscriber are untouched.
102
+ - **Each subscriber is independently testable.** Test `SendConfirmation.call(order: order)` in isolation — no service, no other subscribers.
103
+ - **Error isolation.** If analytics tracking fails, the email still sends and the warehouse still gets notified. One subscriber's failure doesn't cascade.
104
+ - **Replaces callback chains.** Instead of 5 `after_create` callbacks on the model, there are 5 focused subscriber classes registered in one place.
105
+
106
+ ## Anti-Pattern
107
+
108
+ Using model callbacks as an implicit observer pattern:
109
+
110
+ ```ruby
111
+ class Order < ApplicationRecord
112
+ after_create :send_confirmation
113
+ after_create :notify_warehouse
114
+ after_create :track_analytics
115
+ after_create :update_inventory
116
+ after_create :award_loyalty_points
117
+
118
+ private
119
+
120
+ def send_confirmation
121
+ OrderMailer.confirmation(self).deliver_later
122
+ end
123
+
124
+ def notify_warehouse
125
+ WarehouseApi.notify(id: id)
126
+ end
127
+
128
+ # ... 30 more lines of callback methods
129
+ end
130
+ ```
131
+
132
+ ## Why This Is Bad
133
+
134
+ - **Tightly coupled.** Every subscriber is a method on the model. Adding a new reaction means modifying the model class.
135
+ - **Hidden execution order.** Callbacks run in declaration order, but that's not obvious. Reordering lines changes behavior silently.
136
+ - **Can't skip selectively.** Creating an order in seeds or tests triggers ALL callbacks. There's no way to say "create without notifications."
137
+ - **Transaction danger.** `after_create` runs inside the transaction. If `notify_warehouse` raises, the entire order creation rolls back.
138
+
139
+ ## When To Apply
140
+
141
+ - **Multiple side effects triggered by one action.** An order is created → send email, notify warehouse, track analytics, update inventory. Each side effect is a subscriber.
142
+ - **Different teams own different reactions.** The billing team owns payment processing, the ops team owns warehouse notifications, the marketing team owns analytics. Each team's code is a separate subscriber.
143
+ - **You want to add/remove reactions without touching the core flow.** Feature flags can enable/disable subscribers without modifying the publisher.
144
+
145
+ ## When NOT To Apply
146
+
147
+ - **One or two simple side effects.** If creating an order only sends one email, a direct call in the service object is clearer than an event bus.
148
+ - **Synchronous, transactional requirements.** If the side effect MUST succeed for the action to succeed (deducting credits must happen for the AI response to be valid), use direct calls within a transaction — not events.
149
+ - **Don't build an event bus for 3 events.** The overhead of a custom event system isn't justified until you have 10+ events with multiple subscribers each.
150
+
151
+ ## Rails-Specific Alternatives
152
+
153
+ **`after_commit` for job enqueueing:**
154
+ If you want callback-style simplicity with event-style decoupling:
155
+
156
+ ```ruby
157
+ class Order < ApplicationRecord
158
+ after_commit :publish_created_event, on: :create
159
+
160
+ private
161
+
162
+ def publish_created_event
163
+ OrderCreatedJob.perform_later(id)
164
+ end
165
+ end
166
+
167
+ # The job dispatches to handlers
168
+ class OrderCreatedJob < ApplicationJob
169
+ def perform(order_id)
170
+ order = Order.find(order_id)
171
+ OrderCreatedHandlers::SendConfirmation.call(order: order)
172
+ OrderCreatedHandlers::NotifyWarehouse.call(order: order)
173
+ end
174
+ end
175
+ ```
176
+
177
+ This is pragmatic for small apps — it uses Rails conventions while keeping handlers extracted.
@@ -0,0 +1,140 @@
1
+ # Design Pattern: Proxy
2
+
3
+ ## Pattern
4
+
5
+ Provide a surrogate or placeholder for another object to control access to it. Proxies add a layer between the client and the real object — for lazy loading, access control, logging, or caching — without the client knowing the difference.
6
+
7
+ ```ruby
8
+ # Caching proxy — caches expensive API calls
9
+ class CachingEmbeddingProxy
10
+ def initialize(real_client, cache: Rails.cache, ttl: 24.hours)
11
+ @real_client = real_client
12
+ @cache = cache
13
+ @ttl = ttl
14
+ end
15
+
16
+ def embed(texts)
17
+ cache_key = "embeddings:#{Digest::SHA256.hexdigest(texts.sort.join('|'))}"
18
+
19
+ @cache.fetch(cache_key, expires_in: @ttl) do
20
+ @real_client.embed(texts)
21
+ end
22
+ end
23
+
24
+ def embed_query(text)
25
+ # Queries are unique per request — don't cache
26
+ @real_client.embed_query(text)
27
+ end
28
+ end
29
+
30
+ # Usage — caller doesn't know it's a proxy
31
+ client = Embeddings::HttpClient.new(base_url: ENV["EMBEDDING_URL"])
32
+ client = CachingEmbeddingProxy.new(client)
33
+ vectors = client.embed(["class Order; end"]) # Cached after first call
34
+ ```
35
+
36
+ ```ruby
37
+ # Access control proxy — checks permissions before delegating
38
+ class AuthorizingProjectProxy
39
+ def initialize(project, user)
40
+ @project = project
41
+ @user = user
42
+ @membership = project.project_memberships.find_by(user: user)
43
+ end
44
+
45
+ def code_embeddings
46
+ require_role!(:viewer)
47
+ @project.code_embeddings
48
+ end
49
+
50
+ def update!(attributes)
51
+ require_role!(:admin)
52
+ @project.update!(attributes)
53
+ end
54
+
55
+ def destroy!
56
+ require_role!(:owner)
57
+ @project.destroy!
58
+ end
59
+
60
+ def method_missing(method, ...)
61
+ require_role!(:viewer)
62
+ @project.public_send(method, ...)
63
+ end
64
+
65
+ def respond_to_missing?(method, include_private = false)
66
+ @project.respond_to?(method, include_private)
67
+ end
68
+
69
+ private
70
+
71
+ ROLE_HIERARCHY = { viewer: 0, member: 1, admin: 2, owner: 3 }.freeze
72
+
73
+ def require_role!(minimum)
74
+ current = ROLE_HIERARCHY[@membership&.role&.to_sym] || -1
75
+ required = ROLE_HIERARCHY[minimum]
76
+
77
+ raise Forbidden, "Requires #{minimum} role" if current < required
78
+ end
79
+ end
80
+
81
+ # Usage
82
+ project = AuthorizingProjectProxy.new(project, current_user)
83
+ project.code_embeddings # Works for viewer+
84
+ project.update!(name: "New Name") # Only admin+
85
+ project.destroy! # Only owner
86
+ ```
87
+
88
+ ```ruby
89
+ # Lazy loading proxy — defers expensive initialization
90
+ class LazyModelProxy
91
+ def initialize(&loader)
92
+ @loader = loader
93
+ @loaded = false
94
+ @target = nil
95
+ end
96
+
97
+ def method_missing(method, ...)
98
+ load_target!
99
+ @target.public_send(method, ...)
100
+ end
101
+
102
+ def respond_to_missing?(method, include_private = false)
103
+ load_target!
104
+ @target.respond_to?(method, include_private)
105
+ end
106
+
107
+ private
108
+
109
+ def load_target!
110
+ unless @loaded
111
+ @target = @loader.call
112
+ @loaded = true
113
+ end
114
+ end
115
+ end
116
+
117
+ # Usage — the DB query only runs when you access the object
118
+ expensive_report = LazyModelProxy.new { Report.generate_monthly(Date.current) }
119
+ # No query yet...
120
+ expensive_report.total # NOW the query runs
121
+ ```
122
+
123
+ ## Why This Is Good
124
+
125
+ - **Transparent to the caller.** The proxy responds to the same methods as the real object. Code that uses the real client works unchanged with the caching proxy.
126
+ - **Separation of concerns.** Caching logic lives in the proxy, not in the client. Auth logic lives in the auth proxy, not in the model.
127
+ - **Composable with other patterns.** A caching proxy can wrap a logging decorator which wraps the real client. Each layer adds one concern.
128
+
129
+ ## When To Apply
130
+
131
+ - **Caching expensive operations.** API calls, database queries, computations.
132
+ - **Access control.** Check permissions before allowing operations on a resource.
133
+ - **Lazy loading.** Defer initialization of expensive objects until they're actually used.
134
+ - **Remote objects.** Wrap a remote API to look like a local object.
135
+
136
+ ## When NOT To Apply
137
+
138
+ - **Simple delegation.** If you're just forwarding calls without adding behavior, use `delegate` or `SimpleDelegator` — not a proxy.
139
+ - **Decorator already fits.** Proxies control access. Decorators add behavior. If you're adding behavior (logging, metrics), use a decorator.
140
+ - **The object is cheap to create.** Lazy loading a simple `User.new` adds complexity without benefit.
@@ -0,0 +1,124 @@
1
+ # Design Pattern: Singleton
2
+
3
+ ## Pattern
4
+
5
+ Ensure a class has only one instance and provide a global point of access to it. Ruby provides a built-in `Singleton` module, but in practice you should almost always use module-level state or class methods instead.
6
+
7
+ ### Ruby's Built-In Singleton
8
+
9
+ ```ruby
10
+ require "singleton"
11
+
12
+ class AppConfig
13
+ include Singleton
14
+
15
+ attr_accessor :api_key, :environment, :log_level
16
+
17
+ def initialize
18
+ @environment = ENV.fetch("RACK_ENV", "development")
19
+ @log_level = :info
20
+ end
21
+ end
22
+
23
+ # Usage
24
+ AppConfig.instance.api_key = "sk-123"
25
+ AppConfig.instance.log_level # => :info
26
+
27
+ # .new raises NoMethodError
28
+ AppConfig.new # => NoMethodError: private method 'new' called
29
+ ```
30
+
31
+ ### Better Alternative: Module with State
32
+
33
+ ```ruby
34
+ # More idiomatic Ruby — module with class-level state
35
+ module AppConfig
36
+ class << self
37
+ attr_accessor :api_key, :environment, :log_level
38
+
39
+ def configure
40
+ yield self if block_given?
41
+ end
42
+
43
+ def reset!
44
+ @api_key = nil
45
+ @environment = "development"
46
+ @log_level = :info
47
+ end
48
+ end
49
+
50
+ # Defaults
51
+ self.environment = ENV.fetch("RACK_ENV", "development")
52
+ self.log_level = :info
53
+ end
54
+
55
+ # Usage — cleaner, no .instance call
56
+ AppConfig.configure do |config|
57
+ config.api_key = "sk-123"
58
+ config.log_level = :debug
59
+ end
60
+
61
+ AppConfig.api_key # => "sk-123"
62
+ ```
63
+
64
+ ### Thread-Safe Singleton (When You Actually Need One)
65
+
66
+ ```ruby
67
+ class ConnectionPool
68
+ include Singleton
69
+
70
+ def initialize
71
+ @mutex = Mutex.new
72
+ @connections = []
73
+ @max_size = 10
74
+ end
75
+
76
+ def checkout
77
+ @mutex.synchronize do
78
+ @connections.pop || create_connection
79
+ end
80
+ end
81
+
82
+ def checkin(conn)
83
+ @mutex.synchronize do
84
+ @connections.push(conn) if @connections.size < @max_size
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def create_connection
91
+ DatabaseConnection.new
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## When To Apply
97
+
98
+ - **Connection pools.** A single pool managing shared resources across threads.
99
+ - **Configuration objects.** Global app configuration accessed everywhere (but prefer the module pattern above).
100
+ - **Caches.** A single in-memory cache shared across the application.
101
+ - **Logger instances.** One logger configured once, used everywhere.
102
+
103
+ ## When NOT To Apply (Most of the Time)
104
+
105
+ - **Don't use Singleton as a global variable.** If you're using Singleton to share state between unrelated classes, you have a coupling problem. Pass dependencies explicitly.
106
+ - **Don't use Singleton in Rails.** Rails has `Rails.application.config`, `Rails.cache`, `Rails.logger`. Use those instead of rolling your own singletons.
107
+ - **Don't use Singleton for testability-killing global state.** Singletons persist across tests, causing test pollution. Module-level state with a `reset!` method is easier to test.
108
+ - **Prefer dependency injection.** Instead of `AppConfig.instance.api_key` deep inside a service, pass the API key as a constructor argument. This makes the dependency explicit and testable.
109
+
110
+ ## Edge Cases
111
+
112
+ **Singleton + Threads:**
113
+ Ruby's `Singleton` module is thread-safe for instance creation. But the instance's mutable state is NOT thread-safe unless you add synchronization (`Mutex`).
114
+
115
+ **Testing Singletons:**
116
+ Always provide a `reset!` method:
117
+
118
+ ```ruby
119
+ def teardown
120
+ AppConfig.reset!
121
+ end
122
+ ```
123
+
124
+ Without this, state leaks between tests and causes order-dependent failures.
@@ -0,0 +1,207 @@
1
+ # Design Pattern: State
2
+
3
+ ## Pattern
4
+
5
+ Allow an object to change its behavior when its internal state changes by delegating to state objects. Instead of `case` statements on a status field, each state is a class that defines the valid transitions and behavior for that state.
6
+
7
+ ```ruby
8
+ # State classes — each defines what's possible in that state
9
+ module OrderStates
10
+ class Pending
11
+ def confirm(order)
12
+ return Result.new(success: false, error: "No items") if order.line_items.empty?
13
+
14
+ order.update!(status: :confirmed, confirmed_at: Time.current)
15
+ OrderMailer.confirmed(order).deliver_later
16
+ Result.new(success: true)
17
+ end
18
+
19
+ def cancel(order, reason:)
20
+ order.update!(status: :cancelled, cancelled_at: Time.current, cancel_reason: reason)
21
+ Result.new(success: true)
22
+ end
23
+
24
+ def ship(_order) = Result.new(success: false, error: "Cannot ship a pending order")
25
+ def deliver(_order) = Result.new(success: false, error: "Cannot deliver a pending order")
26
+ end
27
+
28
+ class Confirmed
29
+ def confirm(_order) = Result.new(success: false, error: "Already confirmed")
30
+
31
+ def ship(order)
32
+ order.update!(status: :shipped, shipped_at: Time.current)
33
+ OrderMailer.shipped(order).deliver_later
34
+ Result.new(success: true)
35
+ end
36
+
37
+ def cancel(order, reason:)
38
+ order.update!(status: :cancelled, cancelled_at: Time.current, cancel_reason: reason)
39
+ Orders::RefundService.call(order)
40
+ Result.new(success: true)
41
+ end
42
+
43
+ def deliver(_order) = Result.new(success: false, error: "Must ship before delivering")
44
+ end
45
+
46
+ class Shipped
47
+ def confirm(_order) = Result.new(success: false, error: "Already shipped")
48
+ def ship(_order) = Result.new(success: false, error: "Already shipped")
49
+ def cancel(_order, reason: nil) = Result.new(success: false, error: "Cannot cancel shipped order")
50
+
51
+ def deliver(order)
52
+ order.update!(status: :delivered, delivered_at: Time.current)
53
+ OrderMailer.delivered(order).deliver_later
54
+ Result.new(success: true)
55
+ end
56
+ end
57
+
58
+ class Delivered
59
+ def confirm(_order) = Result.new(success: false, error: "Already delivered")
60
+ def ship(_order) = Result.new(success: false, error: "Already delivered")
61
+ def cancel(_order, reason: nil) = Result.new(success: false, error: "Cannot cancel delivered order")
62
+ def deliver(_order) = Result.new(success: false, error: "Already delivered")
63
+ end
64
+
65
+ class Cancelled
66
+ def confirm(_order) = Result.new(success: false, error: "Order is cancelled")
67
+ def ship(_order) = Result.new(success: false, error: "Order is cancelled")
68
+ def cancel(_order, reason: nil) = Result.new(success: false, error: "Already cancelled")
69
+ def deliver(_order) = Result.new(success: false, error: "Order is cancelled")
70
+ end
71
+
72
+ MAPPING = {
73
+ "pending" => Pending.new,
74
+ "confirmed" => Confirmed.new,
75
+ "shipped" => Shipped.new,
76
+ "delivered" => Delivered.new,
77
+ "cancelled" => Cancelled.new
78
+ }.freeze
79
+
80
+ def self.for(status)
81
+ MAPPING.fetch(status)
82
+ end
83
+ end
84
+
85
+ # The Order delegates state-dependent behavior
86
+ class Order < ApplicationRecord
87
+ def current_state
88
+ OrderStates.for(status)
89
+ end
90
+
91
+ def confirm!
92
+ current_state.confirm(self)
93
+ end
94
+
95
+ def ship!
96
+ current_state.ship(self)
97
+ end
98
+
99
+ def cancel!(reason:)
100
+ current_state.cancel(self, reason: reason)
101
+ end
102
+
103
+ def deliver!
104
+ current_state.deliver(self)
105
+ end
106
+ end
107
+
108
+ # Usage is clean and safe
109
+ order = Order.find(params[:id])
110
+ result = order.confirm! # Works when pending
111
+ result = order.ship! # Works when confirmed
112
+ result = order.cancel!(reason: "changed mind") # Invalid when shipped
113
+ # result.success? => false, result.error => "Cannot cancel shipped order"
114
+ ```
115
+
116
+ ## Why This Is Good
117
+
118
+ - **Invalid transitions return errors, not crashes.** Calling `ship!` on a pending order returns a descriptive error instead of silently doing nothing or raising an exception.
119
+ - **Each state's rules are visible in one place.** Open `Confirmed` to see everything that can happen from the confirmed state. No scanning a 200-line model for scattered `if status == "confirmed"` checks.
120
+ - **Adding a new state means adding one class.** A `Refunded` state is one new class with 4 methods. Existing states don't change.
121
+ - **Testable per state.** Test `Pending#confirm` in isolation — does it update status, send email, return success? Test `Shipped#cancel` — does it return the right error?
122
+
123
+ ## Anti-Pattern
124
+
125
+ A case/when on status scattered throughout the model:
126
+
127
+ ```ruby
128
+ class Order < ApplicationRecord
129
+ def confirm!
130
+ case status
131
+ when "pending"
132
+ update!(status: :confirmed, confirmed_at: Time.current)
133
+ OrderMailer.confirmed(self).deliver_later
134
+ when "confirmed"
135
+ raise "Already confirmed"
136
+ when "shipped", "delivered"
137
+ raise "Cannot confirm — already #{status}"
138
+ when "cancelled"
139
+ raise "Cannot confirm cancelled order"
140
+ end
141
+ end
142
+
143
+ def ship!
144
+ case status
145
+ when "confirmed"
146
+ update!(status: :shipped, shipped_at: Time.current)
147
+ when "pending"
148
+ raise "Must confirm first"
149
+ # ... another 10 lines of case/when
150
+ end
151
+ end
152
+
153
+ def cancel!
154
+ case status
155
+ when "pending", "confirmed"
156
+ update!(status: :cancelled)
157
+ Orders::RefundService.call(self) if status == "confirmed"
158
+ when "shipped"
159
+ raise "Cannot cancel shipped order"
160
+ # ... more branching
161
+ end
162
+ end
163
+ end
164
+ ```
165
+
166
+ ## Why This Is Bad
167
+
168
+ - **N methods × M states = N×M branches.** 4 actions × 5 states = 20 case branches scattered across 4 methods. Adding a 6th state means editing all 4 methods.
169
+ - **Rules for one state are split across multiple methods.** To understand "what can a confirmed order do?" you read `confirm!`, `ship!`, `cancel!`, and `deliver!` — scanning for `when "confirmed"` in each.
170
+ - **Inconsistent error handling.** Some branches raise, some return nil, some silently do nothing. The State pattern enforces a consistent return type (`Result`).
171
+
172
+ ## When To Apply
173
+
174
+ - **An object has 3+ states with different behavior.** Orders (pending/confirmed/shipped/delivered/cancelled), subscriptions (trialing/active/past_due/cancelled), projects (draft/active/archived).
175
+ - **You find yourself writing `case status` or `if object.pending?` in multiple places.** That's the State pattern trying to emerge.
176
+ - **State transitions have side effects.** Confirming sends an email, shipping notifies the warehouse, cancelling triggers a refund. Each state's transitions have different side effects.
177
+
178
+ ## When NOT To Apply
179
+
180
+ - **Two states with simple behavior.** An `active`/`inactive` boolean with one behavior difference doesn't need state objects. A simple `if active?` is clearer.
181
+ - **Status is display-only.** If the status field only affects what badge is shown in the UI, a helper method or enum is sufficient.
182
+ - **The team uses `aasm` or `statesman` gems.** Follow existing conventions. These gems implement the State pattern with DSL sugar.
183
+
184
+ ## Edge Cases
185
+
186
+ **State machine gems vs hand-rolled:**
187
+ For simple state machines (3-5 states, clear transitions), hand-rolled state objects are clearer. For complex machines (10+ states, guards, audit trails), consider `statesman` or `aasm`.
188
+
189
+ **Querying by state:**
190
+ State objects handle behavior. Database queries use the status column directly:
191
+
192
+ ```ruby
193
+ scope :actionable, -> { where(status: %w[pending confirmed]) }
194
+ scope :completed, -> { where(status: %w[delivered cancelled]) }
195
+ ```
196
+
197
+ **Persisting state transitions for audit:**
198
+
199
+ ```ruby
200
+ class OrderStates::Confirmed
201
+ def ship(order)
202
+ order.update!(status: :shipped, shipped_at: Time.current)
203
+ order.state_transitions.create!(from: "confirmed", to: "shipped", actor: Current.user)
204
+ Result.new(success: true)
205
+ end
206
+ end
207
+ ```