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,176 @@
1
+ # Code Quality: YAGNI — You Ain't Gonna Need It
2
+
3
+ ## Core Principle
4
+
5
+ Don't build it until you need it. Every line of code is a liability — it must be tested, maintained, and understood. Code that exists "in case we need it later" is code that costs you now and may never pay off.
6
+
7
+ ## The Three Questions
8
+
9
+ Before adding abstraction, ask:
10
+
11
+ 1. **Do I need this right now, or might I need it later?** If "later," don't build it.
12
+ 2. **Am I building for the problem I have, or the problem I imagine?** Solve the real problem.
13
+ 3. **What's the cost of adding this later when I actually need it?** Usually low. Build then.
14
+
15
+ ## Premature Abstraction
16
+
17
+ ```ruby
18
+ # YAGNI VIOLATION: Building a plugin system for 1 payment provider
19
+ class PaymentProcessorFactory
20
+ REGISTRY = {}
21
+
22
+ def self.register(name, klass)
23
+ REGISTRY[name] = klass
24
+ end
25
+
26
+ def self.build(name, **config)
27
+ klass = REGISTRY.fetch(name)
28
+ klass.new(**config)
29
+ end
30
+ end
31
+
32
+ class PaymentProcessor
33
+ def charge(amount, token) = raise NotImplementedError
34
+ def refund(transaction_id, amount) = raise NotImplementedError
35
+ def void(transaction_id) = raise NotImplementedError
36
+ end
37
+
38
+ class StripeProcessor < PaymentProcessor
39
+ def charge(amount, token)
40
+ # Stripe-specific code
41
+ end
42
+
43
+ def refund(transaction_id, amount)
44
+ # Stripe-specific code
45
+ end
46
+
47
+ def void(transaction_id)
48
+ # Stripe-specific code
49
+ end
50
+ end
51
+
52
+ PaymentProcessorFactory.register(:stripe, StripeProcessor)
53
+ ```
54
+
55
+ You have ONE payment provider. The factory, the abstract base class, and the registration mechanism add ~40 lines of code that provide zero value today. When you actually need a second provider (if you ever do), adding the abstraction takes 30 minutes.
56
+
57
+ ```ruby
58
+ # RIGHT-SIZED: Direct implementation for the one provider you have
59
+ class Payments::StripeService
60
+ def charge(amount_cents, token)
61
+ charge = Stripe::Charge.create(amount: amount_cents, currency: "usd", source: token)
62
+ Result.new(success: true, transaction_id: charge.id)
63
+ rescue Stripe::CardError => e
64
+ Result.new(success: false, error: e.message)
65
+ end
66
+
67
+ def refund(charge_id, amount_cents)
68
+ Stripe::Refund.create(charge: charge_id, amount: amount_cents)
69
+ Result.new(success: true)
70
+ rescue Stripe::InvalidRequestError => e
71
+ Result.new(success: false, error: e.message)
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Speculative Generality Examples
77
+
78
+ ```ruby
79
+ # YAGNI: Config class for 2 settings
80
+ class Configuration
81
+ include Singleton
82
+ attr_accessor :settings
83
+
84
+ def initialize
85
+ @settings = {}
86
+ end
87
+
88
+ def get(key, default: nil)
89
+ settings.dig(*key.to_s.split(".")) || default
90
+ end
91
+
92
+ def set(key, value)
93
+ keys = key.to_s.split(".")
94
+ hash = keys[0..-2].reduce(settings) { |h, k| h[k] ||= {} }
95
+ hash[keys.last] = value
96
+ end
97
+ end
98
+
99
+ # You only have: API key and model name. Just use ENV.
100
+ # ENV["ANTHROPIC_API_KEY"] and ENV["MODEL_NAME"] are simpler.
101
+ ```
102
+
103
+ ```ruby
104
+ # YAGNI: Abstract base class with one subclass
105
+ class BaseExporter
106
+ def export(data)
107
+ header + body(data) + footer
108
+ end
109
+
110
+ def header = raise NotImplementedError
111
+ def body(data) = raise NotImplementedError
112
+ def footer = raise NotImplementedError
113
+ end
114
+
115
+ class CsvExporter < BaseExporter
116
+ def header = "id,name,total\n"
117
+ def body(data) = data.map { |r| "#{r.id},#{r.name},#{r.total}" }.join("\n")
118
+ def footer = ""
119
+ end
120
+
121
+ # One exporter doesn't need a base class. Just write CsvExporter directly.
122
+ # When you need PdfExporter, THEN extract the common interface.
123
+ ```
124
+
125
+ ## The Rule of Three
126
+
127
+ Don't abstract until you have three concrete examples:
128
+ 1. **First time:** Just write the code.
129
+ 2. **Second time:** Notice the duplication but tolerate it. Maybe add a comment "similar to X."
130
+ 3. **Third time:** Now extract. You have three examples to inform the right abstraction.
131
+
132
+ ```ruby
133
+ # First time: inline
134
+ class OrdersController
135
+ def export
136
+ csv = orders.map { |o| [o.reference, o.total].join(",") }.join("\n")
137
+ send_data csv, filename: "orders.csv"
138
+ end
139
+ end
140
+
141
+ # Second time: notice similarity, tolerate it
142
+ class InvoicesController
143
+ def export
144
+ csv = invoices.map { |i| [i.number, i.amount].join(",") }.join("\n")
145
+ send_data csv, filename: "invoices.csv"
146
+ end
147
+ end
148
+
149
+ # Third time: NOW extract
150
+ class CsvExporter
151
+ def self.call(records, columns:, filename:)
152
+ csv = records.map { |r| columns.map { |c| r.public_send(c) }.join(",") }.join("\n")
153
+ { data: csv, filename: filename }
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## When YAGNI Doesn't Apply
159
+
160
+ - **Known requirements.** If the spec says "support Stripe and PayPal at launch," build the abstraction. That's not speculative — it's a stated requirement.
161
+ - **Architecture boundaries.** Even with one provider, wrapping external APIs behind an adapter is good practice. The adapter isn't speculative — it isolates you from API changes.
162
+ - **Security and data integrity.** Don't skip input validation because "we'll add it later." Security isn't optional.
163
+ - **Testing infrastructure.** Investing in test helpers, factories, and shared examples pays off immediately — not speculatively.
164
+
165
+ ## The Cost of Abstraction
166
+
167
+ Every abstraction has a cost:
168
+ - **Indirection:** Readers must navigate to another file to understand behavior.
169
+ - **Maintenance:** The abstract interface must be kept in sync with all implementations.
170
+ - **Constraint:** Future implementations are shaped by the abstraction, which was designed without knowledge of their needs.
171
+
172
+ Premature abstraction is worse than no abstraction because it constrains future design based on incomplete information. The right abstraction, built with knowledge of 3+ concrete cases, enables extension. The wrong abstraction, built speculatively, requires fighting against it.
173
+
174
+ ## Practical Test
175
+
176
+ Ask: "If I delete this abstraction and inline the code, does anything get worse?" If no — the abstraction isn't earning its keep. Delete it.
@@ -0,0 +1,191 @@
1
+ # Design Pattern: Adapter
2
+
3
+ ## Pattern
4
+
5
+ Convert the interface of one class into another interface that clients expect. Adapters let classes work together that couldn't otherwise because of incompatible interfaces. In Rails, adapters are essential at system boundaries — wrapping external APIs, gems, and services behind a consistent internal interface.
6
+
7
+ ```ruby
8
+ # Your app's internal interface — what your code expects
9
+ # Contract: .embed(texts) → Array<Array<Float>>
10
+
11
+ # Adapter for the Rubyn embedding service (FastAPI sidecar)
12
+ class Embeddings::RubynAdapter
13
+ def initialize(base_url:)
14
+ @base_url = base_url
15
+ @conn = Faraday.new(url: base_url) do |f|
16
+ f.request :json
17
+ f.response :json
18
+ f.adapter Faraday.default_adapter
19
+ end
20
+ end
21
+
22
+ def embed(texts)
23
+ response = @conn.post("/embed", { texts: texts, prefix: "passage" })
24
+ response.body["embeddings"]
25
+ end
26
+ end
27
+
28
+ # Adapter for OpenAI's embedding API (different URL, auth, response format)
29
+ class Embeddings::OpenAiAdapter
30
+ def initialize(api_key:)
31
+ @conn = Faraday.new(url: "https://api.openai.com") do |f|
32
+ f.request :json
33
+ f.response :json
34
+ f.headers["Authorization"] = "Bearer #{api_key}"
35
+ end
36
+ end
37
+
38
+ def embed(texts)
39
+ response = @conn.post("/v1/embeddings", {
40
+ model: "text-embedding-3-small",
41
+ input: texts
42
+ })
43
+ # OpenAI returns { data: [{ embedding: [...] }, ...] }
44
+ # We normalize to Array<Array<Float>> to match our interface
45
+ response.body["data"]
46
+ .sort_by { |d| d["index"] }
47
+ .map { |d| d["embedding"] }
48
+ end
49
+ end
50
+
51
+ # Adapter for a local model via ONNX Runtime (completely different mechanism)
52
+ class Embeddings::LocalOnnxAdapter
53
+ def initialize(model_path:)
54
+ @session = OnnxRuntime::InferenceSession.new(model_path)
55
+ end
56
+
57
+ def embed(texts)
58
+ inputs = texts.map { |text| tokenize(text) }
59
+ outputs = @session.run(nil, { input_ids: inputs })
60
+ outputs.first # Already Array<Array<Float>>
61
+ end
62
+
63
+ private
64
+
65
+ def tokenize(text)
66
+ # Tokenization logic
67
+ end
68
+ end
69
+
70
+ # Your service code doesn't know or care which adapter it uses
71
+ class Codebase::IndexService
72
+ def initialize(embedder:)
73
+ @embedder = embedder # Any adapter works
74
+ end
75
+
76
+ def call(project, files)
77
+ files.each_slice(10) do |batch|
78
+ contents = batch.map(&:last)
79
+ vectors = @embedder.embed(contents) # Same interface, any provider
80
+ batch.zip(vectors).each do |(path, _), vector|
81
+ project.code_embeddings.upsert(
82
+ { file_path: path, embedding: vector },
83
+ unique_by: [:project_id, :file_path]
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # Wire up in config
91
+ embedder = Embeddings::RubynAdapter.new(base_url: ENV["EMBEDDING_SERVICE_URL"])
92
+ # OR: Embeddings::OpenAiAdapter.new(api_key: ENV["OPENAI_API_KEY"])
93
+ # OR: Embeddings::LocalOnnxAdapter.new(model_path: "models/code-embed.onnx")
94
+
95
+ Codebase::IndexService.new(embedder: embedder).call(project, files)
96
+ ```
97
+
98
+ Adapting a gem's interface to your domain:
99
+
100
+ ```ruby
101
+ # The Stripe gem returns Stripe::Charge objects with their own structure.
102
+ # Your app works with a consistent PaymentResult.
103
+
104
+ PaymentResult = Data.define(:success, :transaction_id, :amount_cents, :error)
105
+
106
+ class Payments::StripeAdapter
107
+ def charge(amount_cents:, token:, description:)
108
+ charge = Stripe::Charge.create(
109
+ amount: amount_cents,
110
+ currency: "usd",
111
+ source: token,
112
+ description: description
113
+ )
114
+ PaymentResult.new(
115
+ success: true,
116
+ transaction_id: charge.id,
117
+ amount_cents: charge.amount,
118
+ error: nil
119
+ )
120
+ rescue Stripe::CardError => e
121
+ PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: e.message)
122
+ rescue Stripe::StripeError => e
123
+ PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: "Payment service error")
124
+ end
125
+ end
126
+
127
+ class Payments::BraintreeAdapter
128
+ def charge(amount_cents:, token:, description:)
129
+ result = Braintree::Transaction.sale(
130
+ amount: (amount_cents / 100.0).round(2),
131
+ payment_method_nonce: token,
132
+ options: { submit_for_settlement: true }
133
+ )
134
+ if result.success?
135
+ PaymentResult.new(
136
+ success: true,
137
+ transaction_id: result.transaction.id,
138
+ amount_cents: (result.transaction.amount * 100).to_i,
139
+ error: nil
140
+ )
141
+ else
142
+ PaymentResult.new(success: false, transaction_id: nil, amount_cents: 0, error: result.message)
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ ## Why This Is Good
149
+
150
+ - **Unified interface across providers.** `embedder.embed(texts)` works identically whether the backend is your Python sidecar, OpenAI, or a local ONNX model. Business logic never sees provider-specific details.
151
+ - **Provider-specific complexity is contained.** OpenAI's response format (`{ data: [{ embedding, index }] }`) is normalized inside the adapter. Nobody else in the codebase deals with that structure.
152
+ - **Swappable at configuration time.** Switching from OpenAI to your own model means changing one line in an initializer. No business logic changes, no test changes.
153
+ - **Error normalization.** Each adapter catches its own exceptions (Stripe::CardError, Braintree errors) and returns a consistent `PaymentResult`. The caller never rescues provider-specific errors.
154
+ - **Gem upgrades are isolated.** If Stripe changes their API, only `StripeAdapter` changes. Every other class in your app is insulated.
155
+
156
+ ## When To Apply
157
+
158
+ - **Every external API integration.** Wrap third-party APIs in adapters from day one. Even if you'll never switch providers, the adapter isolates your code from their API changes.
159
+ - **When migrating between providers.** Build the new adapter, test it, swap the configuration. Both adapters coexist during migration.
160
+ - **Normalizing inconsistent interfaces.** Two gems that do similar things with different method names and return types — adapt them to one internal interface.
161
+
162
+ ## When NOT To Apply
163
+
164
+ - **Internal classes with consistent interfaces.** You don't need an adapter between your own service objects if they already share an interface.
165
+ - **Don't over-abstract stable gems.** If you use Devise and will always use Devise, wrapping every Devise method in an adapter is pointless friction.
166
+ - **Single-use, simple integrations.** A one-off API call in a rake task doesn't need a full adapter class.
167
+
168
+ ## Edge Cases
169
+
170
+ **Adapter + Decorator composition:**
171
+ Adapters normalize the interface. Decorators add cross-cutting behavior. They compose naturally:
172
+
173
+ ```ruby
174
+ embedder = Embeddings::RubynAdapter.new(base_url: url) # Normalize interface
175
+ embedder = Embeddings::RetryDecorator.new(embedder) # Add retry
176
+ embedder = Embeddings::LoggingDecorator.new(embedder) # Add logging
177
+ # Result: logged, retried, normalized embedding calls
178
+ ```
179
+
180
+ **Testing adapters:**
181
+ Test each adapter against a shared example that defines the contract:
182
+
183
+ ```ruby
184
+ RSpec.shared_examples "an embedding adapter" do
185
+ it "returns an array of float arrays" do
186
+ result = subject.embed(["def hello; end"])
187
+ expect(result).to be_an(Array)
188
+ expect(result.first).to all(be_a(Float))
189
+ end
190
+ end
191
+ ```
@@ -0,0 +1,254 @@
1
+ # Design Pattern: Bridge
2
+
3
+ ## Pattern
4
+
5
+ Separate an abstraction from its implementation so the two can vary independently. In Ruby, this means composing objects rather than inheriting — passing the implementation as a dependency.
6
+
7
+ ```ruby
8
+ # The "abstraction" — what the caller interacts with
9
+ class NotificationSender
10
+ def initialize(transport:, formatter:)
11
+ @transport = transport # HOW to send (email, SMS, push)
12
+ @formatter = formatter # HOW to format (plain, HTML, markdown)
13
+ end
14
+
15
+ def send(user, event)
16
+ message = @formatter.format(event)
17
+ @transport.deliver(user, message)
18
+ end
19
+ end
20
+
21
+ # Transports (one dimension of variation)
22
+ class EmailTransport
23
+ def deliver(user, message)
24
+ NotificationMailer.send(to: user.email, body: message).deliver_later
25
+ end
26
+ end
27
+
28
+ class SmsTransport
29
+ def deliver(user, message)
30
+ SmsClient.send(user.phone, message.truncate(160))
31
+ end
32
+ end
33
+
34
+ # Formatters (another dimension of variation)
35
+ class PlainFormatter
36
+ def format(event)
37
+ "#{event.title}: #{event.description}"
38
+ end
39
+ end
40
+
41
+ class HtmlFormatter
42
+ def format(event)
43
+ "<h2>#{event.title}</h2><p>#{event.description}</p>"
44
+ end
45
+ end
46
+
47
+ # Mix and match independently — 2 transports × 2 formatters = 4 combinations
48
+ # Without Bridge, you'd need: EmailPlainNotifier, EmailHtmlNotifier,
49
+ # SmsPlainNotifier, SmsHtmlNotifier — and N×M more as you add options
50
+
51
+ sender = NotificationSender.new(transport: EmailTransport.new, formatter: HtmlFormatter.new)
52
+ sender.send(user, order_confirmed_event)
53
+ ```
54
+
55
+ **When to use:** When you have two or more dimensions of variation that would otherwise create an explosion of subclasses.
56
+
57
+ ---
58
+
59
+ # Design Pattern: Memento
60
+
61
+ ## Pattern
62
+
63
+ Capture an object's internal state so it can be restored later, without exposing the internals. Useful for undo, versioning, and audit trails.
64
+
65
+ ```ruby
66
+ # Memento — a frozen snapshot of state
67
+ class OrderMemento
68
+ attr_reader :state, :created_at
69
+
70
+ def initialize(order)
71
+ @state = {
72
+ status: order.status,
73
+ total: order.total,
74
+ shipping_address: order.shipping_address,
75
+ discount_amount: order.discount_amount,
76
+ notes: order.notes
77
+ }.freeze
78
+ @created_at = Time.current
79
+ freeze
80
+ end
81
+ end
82
+
83
+ # Originator — the object that creates and restores from mementos
84
+ class Order < ApplicationRecord
85
+ def save_snapshot
86
+ OrderMemento.new(self)
87
+ end
88
+
89
+ def restore_from(memento)
90
+ assign_attributes(memento.state)
91
+ save!
92
+ end
93
+ end
94
+
95
+ # Caretaker — manages the history of mementos
96
+ class OrderHistory
97
+ def initialize
98
+ @snapshots = []
99
+ end
100
+
101
+ def push(memento)
102
+ @snapshots.push(memento)
103
+ end
104
+
105
+ def pop
106
+ @snapshots.pop
107
+ end
108
+
109
+ def peek
110
+ @snapshots.last
111
+ end
112
+
113
+ def size
114
+ @snapshots.size
115
+ end
116
+ end
117
+
118
+ # Usage — admin makes changes with undo support
119
+ history = OrderHistory.new
120
+ order = Order.find(params[:id])
121
+
122
+ # Save state before changes
123
+ history.push(order.save_snapshot)
124
+ order.update!(status: :shipped, notes: "Expedited shipping")
125
+
126
+ # Oops, wrong order — undo
127
+ previous = history.pop
128
+ order.restore_from(previous)
129
+ # Order is back to its previous state
130
+ ```
131
+
132
+ **When to use:** Undo/redo, draft saving, version history, audit trails where you need to restore previous state.
133
+
134
+ **Rails built-in alternative:** The `paper_trail` gem provides automatic versioning with mementos stored in the database.
135
+
136
+ ---
137
+
138
+ # Design Pattern: Visitor
139
+
140
+ ## Pattern
141
+
142
+ Separate an algorithm from the objects it operates on. Define operations in visitor objects, and let each element "accept" the visitor. This lets you add new operations without modifying the element classes.
143
+
144
+ ```ruby
145
+ # Elements — domain objects that accept visitors
146
+ class Order < ApplicationRecord
147
+ def accept(visitor)
148
+ visitor.visit_order(self)
149
+ end
150
+ end
151
+
152
+ class LineItem < ApplicationRecord
153
+ def accept(visitor)
154
+ visitor.visit_line_item(self)
155
+ end
156
+ end
157
+
158
+ class Discount < ApplicationRecord
159
+ def accept(visitor)
160
+ visitor.visit_discount(self)
161
+ end
162
+ end
163
+
164
+ # Visitor 1: Calculate tax differently per element type
165
+ class TaxCalculatorVisitor
166
+ attr_reader :total_tax
167
+
168
+ def initialize(tax_rate:)
169
+ @tax_rate = tax_rate
170
+ @total_tax = 0
171
+ end
172
+
173
+ def visit_order(order)
174
+ order.line_items.each { |item| item.accept(self) }
175
+ order.discounts.each { |discount| discount.accept(self) }
176
+ end
177
+
178
+ def visit_line_item(item)
179
+ @total_tax += (item.quantity * item.unit_price * @tax_rate).round
180
+ end
181
+
182
+ def visit_discount(discount)
183
+ @total_tax -= (discount.amount * @tax_rate).round
184
+ end
185
+ end
186
+
187
+ # Visitor 2: Export elements to different formats
188
+ class CsvExportVisitor
189
+ attr_reader :rows
190
+
191
+ def initialize
192
+ @rows = [%w[type reference amount]]
193
+ end
194
+
195
+ def visit_order(order)
196
+ @rows << ["order", order.reference, order.total]
197
+ order.line_items.each { |item| item.accept(self) }
198
+ end
199
+
200
+ def visit_line_item(item)
201
+ @rows << ["line_item", item.product.name, item.quantity * item.unit_price]
202
+ end
203
+
204
+ def visit_discount(discount)
205
+ @rows << ["discount", discount.code, -discount.amount]
206
+ end
207
+
208
+ def to_csv
209
+ @rows.map { |row| row.join(",") }.join("\n")
210
+ end
211
+ end
212
+
213
+ # Usage — different operations, same elements
214
+ tax_visitor = TaxCalculatorVisitor.new(tax_rate: 0.08)
215
+ order.accept(tax_visitor)
216
+ tax_visitor.total_tax # => calculated tax
217
+
218
+ csv_visitor = CsvExportVisitor.new
219
+ order.accept(csv_visitor)
220
+ csv_visitor.to_csv # => CSV string
221
+ ```
222
+
223
+ **When to use:** When you need multiple unrelated operations on a set of element types, and you don't want to pollute the element classes with every operation. Common in compilers, report generators, and data exporters.
224
+
225
+ **When NOT to use:** In most Rails apps. Visitor is powerful but heavyweight. If you have 2-3 operations, methods on the models or service objects are simpler. Visitor shines when you have 5+ operations across 5+ element types.
226
+
227
+ ---
228
+
229
+ ## Ruby Idiomatic Alternative to Visitor
230
+
231
+ Ruby's duck typing often makes the Visitor pattern unnecessary. Instead of the formal accept/visit protocol, use polymorphic method dispatch:
232
+
233
+ ```ruby
234
+ # Simpler Ruby approach — no accept/visit ceremony
235
+ class ReportGenerator
236
+ def generate(elements)
237
+ elements.each do |element|
238
+ case element
239
+ when Order then process_order(element)
240
+ when LineItem then process_line_item(element)
241
+ when Discount then process_discount(element)
242
+ end
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ def process_order(order) = # ...
249
+ def process_line_item(item) = # ...
250
+ def process_discount(discount) = # ...
251
+ end
252
+ ```
253
+
254
+ This is less "pure" OOP but more idiomatic Ruby. Use the formal Visitor when the element hierarchy is stable but operations change frequently.