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,133 @@
1
+ # Design Pattern: Facade
2
+
3
+ ## Pattern
4
+
5
+ Provide a simplified interface to a complex subsystem. The facade hides the complexity of multiple classes, APIs, or steps behind a single method call. In Rails, service objects often act as facades over multi-step operations.
6
+
7
+ ```ruby
8
+ # The subsystem is complex — embedding client, chunker, database, change detection
9
+ # The facade makes it one call
10
+
11
+ class Codebase::IndexFacade
12
+ def initialize(
13
+ embedding_client: Rails.application.config.x.embedding_client,
14
+ chunker: Codebase::Chunker.new,
15
+ change_detector: Codebase::ChangeDetector.new
16
+ )
17
+ @embedding_client = embedding_client
18
+ @chunker = chunker
19
+ @change_detector = change_detector
20
+ end
21
+
22
+ # One method hides 5 subsystem interactions
23
+ def index_project(project, files)
24
+ changed_files = @change_detector.filter_changed(project, files)
25
+ return { indexed: 0, skipped: files.size } if changed_files.empty?
26
+
27
+ changed_files.each_slice(10) do |batch|
28
+ chunks = batch.flat_map { |path, content| @chunker.split(path, content) }
29
+ vectors = @embedding_client.embed(chunks.map(&:text))
30
+
31
+ chunks.zip(vectors).each do |chunk, vector|
32
+ project.code_embeddings.upsert(
33
+ {
34
+ file_path: chunk.path,
35
+ chunk_content: chunk.text,
36
+ chunk_type: chunk.type,
37
+ embedding: vector,
38
+ file_hash: chunk.file_hash,
39
+ last_embedded_at: Time.current
40
+ },
41
+ unique_by: [:project_id, :file_path, :chunk_type, :chunk_content]
42
+ )
43
+ end
44
+ end
45
+
46
+ project.update!(last_indexed_at: Time.current)
47
+ { indexed: changed_files.size, skipped: files.size - changed_files.size }
48
+ end
49
+ end
50
+
51
+ # Caller doesn't know about chunkers, change detectors, or embedding clients
52
+ result = Codebase::IndexFacade.new.index_project(project, files)
53
+ puts "Indexed #{result[:indexed]} files, skipped #{result[:skipped]}"
54
+ ```
55
+
56
+ Another example — wrapping a multi-step onboarding process:
57
+
58
+ ```ruby
59
+ class Onboarding::Facade
60
+ def self.call(registration_params)
61
+ new(registration_params).call
62
+ end
63
+
64
+ def initialize(params)
65
+ @params = params
66
+ end
67
+
68
+ def call
69
+ user = create_user
70
+ project = create_default_project(user)
71
+ api_key = generate_api_key(user)
72
+ seed_credits(user)
73
+ send_welcome(user)
74
+
75
+ Result.new(success: true, user: user, project: project, api_key: api_key)
76
+ rescue ActiveRecord::RecordInvalid => e
77
+ Result.new(success: false, error: e.record.errors.full_messages.join(", "))
78
+ end
79
+
80
+ private
81
+
82
+ def create_user
83
+ User.create!(
84
+ email: @params[:email],
85
+ password: @params[:password],
86
+ name: @params[:name]
87
+ )
88
+ end
89
+
90
+ def create_default_project(user)
91
+ project = Project.create!(name: "My First Project")
92
+ ProjectMembership.create!(user: user, project: project, role: :owner)
93
+ project
94
+ end
95
+
96
+ def generate_api_key(user)
97
+ ApiKey.create!(user: user, name: "Default")
98
+ end
99
+
100
+ def seed_credits(user)
101
+ CreditLedger.create!(user: user, amount: 30, description: "Welcome credits")
102
+ end
103
+
104
+ def send_welcome(user)
105
+ WelcomeMailer.welcome(user).deliver_later
106
+ end
107
+ end
108
+
109
+ # Controller is one line
110
+ result = Onboarding::Facade.call(registration_params)
111
+ ```
112
+
113
+ ## Why This Is Good
114
+
115
+ - **One entry point for a complex operation.** `IndexFacade.new.index_project(project, files)` hides change detection, chunking, embedding, upserting, and timestamp updates behind one call.
116
+ - **Subsystem classes remain independent.** `Chunker`, `ChangeDetector`, and `EmbeddingClient` don't know about each other. The facade coordinates them.
117
+ - **Easy to test at two levels.** Integration test: call the facade and assert the database state. Unit tests: test each subsystem class in isolation.
118
+ - **Callers are decoupled from subsystem changes.** If you replace the chunker algorithm, the facade's interface doesn't change. Callers never know.
119
+
120
+ ## When To Apply
121
+
122
+ - **Multi-step operations.** Onboarding, order processing, codebase indexing — anything that coordinates 3+ subsystems.
123
+ - **Complex API integrations.** Wrapping a third-party SDK's 5-step authentication flow behind `Auth::Facade.authenticate(credentials)`.
124
+ - **Simplifying legacy code.** Wrap a messy subsystem in a clean facade before refactoring the internals.
125
+
126
+ ## When NOT To Apply
127
+
128
+ - **Single-step operations.** Wrapping `User.create!(params)` in a facade adds a pointless layer.
129
+ - **Don't hide everything.** If callers sometimes need fine-grained control over individual subsystems, expose them alongside the facade — don't force everything through one entry point.
130
+
131
+ ## Rails Connection
132
+
133
+ Most Rails service objects ARE facades. `Orders::CreateService` is a facade over validation, persistence, payment charging, and notification. The pattern is so common in Rails that we don't always name it — but recognizing it helps design better services.
@@ -0,0 +1,169 @@
1
+ # Design Pattern: Factory Method
2
+
3
+ ## Pattern
4
+
5
+ Define a method that creates objects without specifying the exact class. Let subclasses or configuration determine which class to instantiate. In Ruby, factories are often class methods, configuration hashes, or registry patterns rather than subclass hierarchies.
6
+
7
+ ```ruby
8
+ # Factory method as a class method with registration
9
+ class Notifications::Factory
10
+ REGISTRY = {}
11
+
12
+ def self.register(channel, klass)
13
+ REGISTRY[channel.to_sym] = klass
14
+ end
15
+
16
+ def self.build(channel, **options)
17
+ klass = REGISTRY.fetch(channel.to_sym) do
18
+ raise ArgumentError, "Unknown notification channel: #{channel}. Available: #{REGISTRY.keys.join(', ')}"
19
+ end
20
+ klass.new(**options)
21
+ end
22
+ end
23
+
24
+ class Notifications::EmailNotifier
25
+ def initialize(from: "noreply@rubyn.ai")
26
+ @from = from
27
+ end
28
+
29
+ def deliver(user, message)
30
+ NotificationMailer.notify(to: user.email, from: @from, body: message).deliver_later
31
+ end
32
+ end
33
+ Notifications::Factory.register(:email, Notifications::EmailNotifier)
34
+
35
+ class Notifications::SmsNotifier
36
+ def initialize(provider: :twilio)
37
+ @provider = provider
38
+ end
39
+
40
+ def deliver(user, message)
41
+ SmsClient.new(provider: @provider).send(user.phone, message.truncate(160))
42
+ end
43
+ end
44
+ Notifications::Factory.register(:sms, Notifications::SmsNotifier)
45
+
46
+ # Usage — caller doesn't know or import the concrete class
47
+ notifier = Notifications::Factory.build(:email)
48
+ notifier.deliver(user, "Your order shipped!")
49
+
50
+ notifier = Notifications::Factory.build(:sms, provider: :vonage)
51
+ notifier.deliver(user, "Your order shipped!")
52
+ ```
53
+
54
+ Factory method on a model — named constructors:
55
+
56
+ ```ruby
57
+ class Order < ApplicationRecord
58
+ def self.from_cart(cart, user:)
59
+ new(
60
+ user: user,
61
+ shipping_address: user.default_address,
62
+ line_items: cart.items.map { |item|
63
+ LineItem.new(product: item.product, quantity: item.quantity, unit_price: item.product.price)
64
+ }
65
+ )
66
+ end
67
+
68
+ def self.from_api(params)
69
+ new(
70
+ user: User.find(params[:user_id]),
71
+ shipping_address: params[:shipping_address],
72
+ line_items: params[:items].map { |item|
73
+ LineItem.new(product_id: item[:product_id], quantity: item[:quantity])
74
+ }
75
+ )
76
+ end
77
+
78
+ def self.reorder(previous_order)
79
+ from_cart(
80
+ OpenStruct.new(items: previous_order.line_items),
81
+ user: previous_order.user
82
+ )
83
+ end
84
+ end
85
+
86
+ # Each factory method communicates intent and handles context-specific setup
87
+ order = Order.from_cart(shopping_cart, user: current_user)
88
+ order = Order.from_api(api_params)
89
+ order = Order.reorder(previous_order)
90
+ ```
91
+
92
+ Configuration-driven factory:
93
+
94
+ ```ruby
95
+ # Embedding client factory — environment determines the implementation
96
+ class Embeddings::ClientFactory
97
+ def self.build
98
+ case Rails.env
99
+ when "production"
100
+ Embeddings::HttpClient.new(base_url: ENV.fetch("EMBEDDING_SERVICE_URL"))
101
+ when "test"
102
+ Embeddings::FakeClient.new
103
+ when "development"
104
+ if ENV["EMBEDDING_SERVICE_URL"].present?
105
+ Embeddings::HttpClient.new(base_url: ENV["EMBEDDING_SERVICE_URL"])
106
+ else
107
+ Embeddings::FakeClient.new
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # config/initializers/embeddings.rb
114
+ Rails.application.config.x.embedding_client = Embeddings::ClientFactory.build
115
+ ```
116
+
117
+ ## Why This Is Good
118
+
119
+ - **Decouples creation from use.** The code that uses a notifier doesn't need to know which concrete class to instantiate. It asks the factory for `:email` and gets back a ready-to-use object.
120
+ - **Named constructors are self-documenting.** `Order.from_cart(cart, user:)` tells you exactly what context the order is being created in. `Order.new(user: user, ...)` doesn't.
121
+ - **New types don't require modifying callers.** Adding a `PushNotifier` means registering it with the factory. Every caller that uses `Factory.build(:push)` works immediately.
122
+ - **Environment-aware factories centralize configuration.** One place decides that production uses the real embedding service and tests use a fake.
123
+
124
+ ## When To Apply
125
+
126
+ - **Multiple types with the same interface.** Notification channels, payment processors, export formatters, AI model clients — any family of interchangeable implementations.
127
+ - **Complex construction.** When building an object requires 5+ lines of setup, configuration lookups, or conditional logic, wrap it in a factory method.
128
+ - **Named constructors for models.** When the same model is created from different sources (web form, API, CSV import) with different setup requirements.
129
+
130
+ ## When NOT To Apply
131
+
132
+ - **Simple `new` calls.** `User.new(email: email, name: name)` doesn't need a factory. Factories solve complex or polymorphic construction, not all construction.
133
+ - **One implementation.** If there's only one notifier and will only ever be one, skip the factory. Direct instantiation is clearer.
134
+ - **Rails model `.create` with simple params.** Don't wrap `Order.create!(params)` in a factory just for abstraction. Use factories when there's actual construction complexity.
135
+
136
+ ## Rails Example
137
+
138
+ ```ruby
139
+ # Service object factory for AI interactions — selects strategy based on request type
140
+ class Ai::ServiceFactory
141
+ SERVICES = {
142
+ "refactor" => Ai::RefactorService,
143
+ "review" => Ai::ReviewService,
144
+ "explain" => Ai::ExplainService,
145
+ "generate" => Ai::GenerateService,
146
+ "debug" => Ai::DebugService
147
+ }.freeze
148
+
149
+ def self.build(request_type, **dependencies)
150
+ service_class = SERVICES.fetch(request_type) do
151
+ raise ArgumentError, "Unknown AI request type: #{request_type}"
152
+ end
153
+ service_class.new(**dependencies)
154
+ end
155
+ end
156
+
157
+ # API controller uses the factory
158
+ class Api::V1::AiController < Api::V1::BaseController
159
+ def create
160
+ service = Ai::ServiceFactory.build(
161
+ params[:type],
162
+ client: Rails.application.config.x.ai_client,
163
+ pricing: current_pricing_strategy
164
+ )
165
+ result = service.call(params[:prompt], context: build_context)
166
+ render json: result
167
+ end
168
+ end
169
+ ```
@@ -0,0 +1,116 @@
1
+ # Design Pattern: Iterator
2
+
3
+ ## Pattern
4
+
5
+ Provide a way to traverse elements of a collection without exposing its underlying structure. Ruby's `Enumerable` module IS the Iterator pattern — include it in any class that defines `each`, and you get 50+ traversal methods for free.
6
+
7
+ ```ruby
8
+ # Custom collection with Enumerable — the Ruby way to implement Iterator
9
+ class CodeChunkCollection
10
+ include Enumerable
11
+
12
+ def initialize
13
+ @chunks = []
14
+ end
15
+
16
+ def add(chunk)
17
+ @chunks << chunk
18
+ self
19
+ end
20
+
21
+ # Define `each` — Enumerable gives you everything else
22
+ def each(&block)
23
+ @chunks.each(&block)
24
+ end
25
+
26
+ # Optional: define <=> on elements for sort methods
27
+ def by_relevance(query_embedding)
28
+ sort_by { |chunk| -chunk.similarity_to(query_embedding) }
29
+ end
30
+ end
31
+
32
+ class CodeChunk
33
+ attr_reader :file_path, :content, :embedding, :chunk_type
34
+
35
+ def initialize(file_path:, content:, embedding:, chunk_type:)
36
+ @file_path = file_path
37
+ @content = content
38
+ @embedding = embedding
39
+ @chunk_type = chunk_type
40
+ end
41
+
42
+ def similarity_to(other_embedding)
43
+ dot_product(embedding, other_embedding)
44
+ end
45
+
46
+ private
47
+
48
+ def dot_product(a, b)
49
+ a.zip(b).sum { |x, y| x * y }
50
+ end
51
+ end
52
+
53
+ # Usage — all Enumerable methods work automatically
54
+ chunks = CodeChunkCollection.new
55
+ chunks.add(CodeChunk.new(file_path: "app/models/order.rb", content: "class Order...", embedding: [...], chunk_type: "class"))
56
+ chunks.add(CodeChunk.new(file_path: "app/services/create.rb", content: "class Create...", embedding: [...], chunk_type: "class"))
57
+
58
+ # All these work because we included Enumerable and defined each:
59
+ chunks.map(&:file_path) # ["app/models/order.rb", "app/services/create.rb"]
60
+ chunks.select { |c| c.chunk_type == "class" }
61
+ chunks.count # 2
62
+ chunks.any? { |c| c.file_path.include?("models") }
63
+ chunks.flat_map { |c| c.content.lines }
64
+ chunks.group_by(&:chunk_type)
65
+ chunks.min_by { |c| c.file_path.length }
66
+ ```
67
+
68
+ ### External Iterator with Enumerator
69
+
70
+ ```ruby
71
+ # When you need lazy or external iteration control
72
+ class PaginatedApiIterator
73
+ include Enumerable
74
+
75
+ def initialize(client, endpoint, per_page: 100)
76
+ @client = client
77
+ @endpoint = endpoint
78
+ @per_page = per_page
79
+ end
80
+
81
+ def each
82
+ page = 1
83
+ loop do
84
+ results = @client.get(@endpoint, page: page, per_page: @per_page)
85
+ break if results.empty?
86
+
87
+ results.each { |item| yield item }
88
+ page += 1
89
+ end
90
+ end
91
+ end
92
+
93
+ # Iterate over ALL pages transparently
94
+ iterator = PaginatedApiIterator.new(api_client, "/orders")
95
+ iterator.each { |order| process(order) }
96
+
97
+ # Or use lazily — only fetches pages as needed
98
+ iterator.lazy.select { |o| o["status"] == "pending" }.first(10)
99
+ ```
100
+
101
+ ## Why This Is Good
102
+
103
+ - **Include `Enumerable`, get 50+ methods.** `map`, `select`, `reduce`, `any?`, `none?`, `group_by`, `sort_by`, `flat_map`, `tally`, `min_by`, `max_by`, `chunk`, `each_slice` — all from defining one `each` method.
104
+ - **Lazy evaluation for large/infinite collections.** `.lazy` chains don't materialize intermediate arrays. Process a 10GB file line by line without loading it into memory.
105
+ - **Uniform interface.** Any Enumerable collection works with any method that accepts an Enumerable. Your custom collection is instantly compatible with the entire Ruby ecosystem.
106
+
107
+ ## When To Apply
108
+
109
+ - **Any class that holds a collection.** If your class wraps an array, hash, or tree of objects, include `Enumerable` and define `each`.
110
+ - **Paginated API responses.** Wrap pagination logic in an iterator so callers see a seamless stream of items.
111
+ - **Tree traversal.** Define `each` to walk the tree (depth-first, breadth-first), and all Enumerable methods work on tree nodes.
112
+
113
+ ## When NOT To Apply
114
+
115
+ - **You're just wrapping an Array.** If your class is a thin wrapper around `@items`, consider exposing the array directly or using `delegate :each, :map, :select, to: :items` instead of a full Enumerable include.
116
+ - **ActiveRecord already provides this.** `Order.where(status: :pending).each` — ActiveRecord relations are already iterable. Don't wrap them in another iterator.
@@ -0,0 +1,133 @@
1
+ # Design Pattern: Mediator
2
+
3
+ ## Pattern
4
+
5
+ Reduce chaotic dependencies between objects by centralizing communication through a mediator. Instead of objects referencing each other directly, they communicate through the mediator, which coordinates the interaction.
6
+
7
+ In Rails, this maps to orchestrator services that coordinate multiple subsystems without those subsystems knowing about each other.
8
+
9
+ ```ruby
10
+ # Without Mediator: Objects reference each other directly
11
+ # Order knows about Inventory, Payment, Notification, Analytics
12
+ # Inventory knows about Order, Notification
13
+ # Payment knows about Order, Notification, Analytics
14
+ # Everything is coupled to everything
15
+
16
+ # With Mediator: One orchestrator coordinates everything
17
+ class Orders::CheckoutMediator
18
+ def initialize(
19
+ inventory: Inventory::ReservationService.new,
20
+ payment: Payments::ChargeService.new,
21
+ notification: Notifications::Dispatcher.new,
22
+ analytics: Analytics::Tracker.new
23
+ )
24
+ @inventory = inventory
25
+ @payment = payment
26
+ @notification = notification
27
+ @analytics = analytics
28
+ end
29
+
30
+ def checkout(order, payment_method)
31
+ # Step 1: Reserve inventory
32
+ reservation = @inventory.reserve(order.line_items)
33
+ unless reservation.success?
34
+ return Result.new(success: false, error: "Items unavailable: #{reservation.error}")
35
+ end
36
+
37
+ # Step 2: Charge payment
38
+ charge = @payment.charge(order.total_cents, payment_method.token)
39
+ unless charge.success?
40
+ @inventory.release(reservation.id) # Compensate
41
+ return Result.new(success: false, error: "Payment failed: #{charge.error}")
42
+ end
43
+
44
+ # Step 3: Confirm order
45
+ order.update!(
46
+ status: :confirmed,
47
+ confirmed_at: Time.current,
48
+ payment_transaction_id: charge.transaction_id
49
+ )
50
+
51
+ # Step 4: Side effects (non-critical)
52
+ @notification.dispatch(order.user, "Order #{order.reference} confirmed!")
53
+ @analytics.track("checkout_completed", order_id: order.id, total: order.total)
54
+
55
+ Result.new(success: true, order: order)
56
+ rescue StandardError => e
57
+ # Compensate on unexpected failure
58
+ @inventory.release(reservation&.id) if reservation&.success?
59
+ @payment.refund(charge.transaction_id) if charge&.success?
60
+ raise
61
+ end
62
+ end
63
+
64
+ # Each service is independent — none knows about the others
65
+ class Inventory::ReservationService
66
+ def reserve(line_items)
67
+ # Only knows about inventory
68
+ end
69
+
70
+ def release(reservation_id)
71
+ # Only knows about inventory
72
+ end
73
+ end
74
+
75
+ class Payments::ChargeService
76
+ def charge(amount_cents, token)
77
+ # Only knows about payments
78
+ end
79
+
80
+ def refund(transaction_id)
81
+ # Only knows about payments
82
+ end
83
+ end
84
+ ```
85
+
86
+ ## Why This Is Good
87
+
88
+ - **Subsystems are decoupled.** `Inventory::ReservationService` doesn't know about payments. `Payments::ChargeService` doesn't know about notifications. Each can be developed, tested, and deployed independently.
89
+ - **The workflow is visible in one place.** Open `CheckoutMediator` and read the entire checkout flow: reserve → charge → confirm → notify → track. The orchestration logic is centralized.
90
+ - **Compensation logic is explicit.** If payment fails, inventory is released. If anything unexpected happens, both are rolled back. This saga-style coordination is easy to reason about when it's in one mediator.
91
+ - **Easy to modify the flow.** Adding fraud detection between reserve and charge means adding one step in the mediator. No other services change.
92
+ - **Testable with injected doubles.** Each service is injected, so tests can verify the orchestration without real inventory, payments, or notifications.
93
+
94
+ ## Anti-Pattern
95
+
96
+ Objects communicating directly, creating a web of dependencies:
97
+
98
+ ```ruby
99
+ # BAD: Order model orchestrates everything
100
+ class Order < ApplicationRecord
101
+ after_create :reserve_inventory
102
+ after_create :charge_payment
103
+ after_create :send_notification
104
+ after_create :track_analytics
105
+
106
+ private
107
+
108
+ def reserve_inventory
109
+ Inventory::ReservationService.new.reserve(line_items)
110
+ end
111
+
112
+ def charge_payment
113
+ result = Payments::ChargeService.new.charge(total_cents, user.default_payment_method.token)
114
+ unless result.success?
115
+ Inventory::ReservationService.new.release(inventory_reservation_id)
116
+ raise "Payment failed"
117
+ end
118
+ end
119
+ # ... Order knows about EVERY subsystem
120
+ end
121
+ ```
122
+
123
+ ## When To Apply
124
+
125
+ - **Multi-step workflows** — checkout, registration, order fulfillment, onboarding. Any flow that touches 3+ subsystems.
126
+ - **When objects are becoming too interconnected.** If Service A calls Service B which calls Service C which calls Service A, you need a mediator to break the cycle.
127
+ - **Saga/compensation patterns.** When steps must be rolled back if later steps fail, a mediator manages the compensation logic.
128
+
129
+ ## When NOT To Apply
130
+
131
+ - **Two objects communicating.** A service calling one other service doesn't need a mediator. That's just a method call.
132
+ - **Event-driven communication works better.** If subsystems don't need coordination (just fire-and-forget), use the Observer pattern instead.
133
+ - **The mediator becomes a god object.** If the mediator is 500 lines with 20 dependencies, split it into smaller mediators for sub-workflows.