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,186 @@
1
+ # Gems: dry-rb Ecosystem
2
+
3
+ ## Pattern
4
+
5
+ The dry-rb family provides standalone, composable libraries for validation, types, dependency injection, and functional patterns. They're popular in non-Rails Ruby projects and in Rails apps that want more rigor than ActiveModel provides.
6
+
7
+ ### dry-validation (Schema Validation)
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "dry-validation", "~> 1.10"
12
+
13
+ # app/contracts/order_contract.rb
14
+ class OrderContract < Dry::Validation::Contract
15
+ params do
16
+ required(:shipping_address).filled(:string)
17
+ required(:line_items).array(:hash) do
18
+ required(:product_id).filled(:integer)
19
+ required(:quantity).filled(:integer, gt?: 0)
20
+ end
21
+ optional(:notes).maybe(:string)
22
+ optional(:discount_code).maybe(:string, max_size?: 20)
23
+ end
24
+
25
+ rule(:line_items) do
26
+ key.failure("must have at least one item") if values[:line_items].empty?
27
+ end
28
+
29
+ rule(:discount_code) do
30
+ if values[:discount_code] && !Discount.active.exists?(code: values[:discount_code])
31
+ key.failure("is not a valid discount code")
32
+ end
33
+ end
34
+ end
35
+
36
+ # Usage
37
+ contract = OrderContract.new
38
+ result = contract.call(params)
39
+
40
+ if result.success?
41
+ # result.to_h contains validated, coerced data
42
+ Orders::CreateService.call(result.to_h, current_user)
43
+ else
44
+ # result.errors.to_h contains error messages by field
45
+ # => { line_items: ["must have at least one item"] }
46
+ render json: { errors: result.errors.to_h }, status: :unprocessable_entity
47
+ end
48
+ ```
49
+
50
+ ### dry-types (Type System)
51
+
52
+ ```ruby
53
+ # Gemfile
54
+ gem "dry-types", "~> 1.7"
55
+
56
+ # app/types.rb
57
+ module Types
58
+ include Dry.Types()
59
+
60
+ Email = Types::String.constrained(format: URI::MailTo::EMAIL_REGEXP)
61
+ PositiveMoney = Types::Coercible::Integer.constrained(gteq: 0)
62
+ Status = Types::String.enum("pending", "confirmed", "shipped", "delivered", "cancelled")
63
+ CreditAmount = Types::Coercible::Integer.constrained(gteq: 1, lteq: 10_000)
64
+ end
65
+
66
+ # Usage — type checking and coercion
67
+ Types::Email["alice@example.com"] # => "alice@example.com"
68
+ Types::Email["not-an-email"] # => Dry::Types::ConstraintError
69
+
70
+ Types::PositiveMoney[1999] # => 1999
71
+ Types::PositiveMoney[-5] # => Dry::Types::ConstraintError
72
+
73
+ Types::Status["pending"] # => "pending"
74
+ Types::Status["invalid"] # => Dry::Types::ConstraintError
75
+ ```
76
+
77
+ ### dry-monads (Result Type / Railway Programming)
78
+
79
+ ```ruby
80
+ # Gemfile
81
+ gem "dry-monads", "~> 1.6"
82
+
83
+ # app/services/orders/create_service.rb
84
+ require "dry/monads"
85
+
86
+ class Orders::CreateService
87
+ include Dry::Monads[:result, :do]
88
+
89
+ def call(params, user)
90
+ validated = yield validate(params)
91
+ order = yield create_order(validated, user)
92
+ yield charge_payment(order)
93
+ yield send_confirmation(order)
94
+
95
+ Success(order)
96
+ end
97
+
98
+ private
99
+
100
+ def validate(params)
101
+ result = OrderContract.new.call(params)
102
+ result.success? ? Success(result.to_h) : Failure(result.errors.to_h)
103
+ end
104
+
105
+ def create_order(params, user)
106
+ order = user.orders.create(params)
107
+ order.persisted? ? Success(order) : Failure(order.errors.full_messages)
108
+ end
109
+
110
+ def charge_payment(order)
111
+ result = Payments::ChargeService.call(order)
112
+ result.success? ? Success(result) : Failure("Payment failed: #{result.error}")
113
+ end
114
+
115
+ def send_confirmation(order)
116
+ OrderMailer.confirmation(order).deliver_later
117
+ Success(true)
118
+ end
119
+ end
120
+
121
+ # Controller — pattern match on the result
122
+ result = Orders::CreateService.new.call(params, current_user)
123
+
124
+ case result
125
+ in Dry::Monads::Success(order)
126
+ redirect_to order, notice: "Order placed"
127
+ in Dry::Monads::Failure(errors) if errors.is_a?(Hash)
128
+ @errors = errors
129
+ render :new, status: :unprocessable_entity
130
+ in Dry::Monads::Failure(message)
131
+ flash.now[:alert] = message
132
+ render :new, status: :unprocessable_entity
133
+ end
134
+ ```
135
+
136
+ The `yield` keyword with `do` notation short-circuits on `Failure` — if `validate` returns `Failure`, the remaining steps never execute. This is the "railway" pattern: success continues down the track, failure jumps to the error track immediately.
137
+
138
+ ### dry-struct (Typed Data Objects)
139
+
140
+ ```ruby
141
+ # Gemfile
142
+ gem "dry-struct", "~> 1.6"
143
+
144
+ class OrderRequest < Dry::Struct
145
+ attribute :shipping_address, Types::String
146
+ attribute :line_items, Types::Array.of(LineItemRequest)
147
+ attribute :discount_code, Types::String.optional
148
+ attribute :notes, Types::String.optional.default(nil)
149
+ end
150
+
151
+ class LineItemRequest < Dry::Struct
152
+ attribute :product_id, Types::Coercible::Integer
153
+ attribute :quantity, Types::Coercible::Integer.constrained(gteq: 1)
154
+ end
155
+
156
+ # Construction validates types automatically
157
+ request = OrderRequest.new(
158
+ shipping_address: "123 Main St",
159
+ line_items: [{ product_id: "42", quantity: "2" }], # Strings coerced to integers
160
+ discount_code: nil
161
+ )
162
+
163
+ request.line_items.first.product_id # => 42 (Integer, coerced from String)
164
+ request.line_items.first.quantity # => 2 (Integer)
165
+ ```
166
+
167
+ ## Why This Is Good
168
+
169
+ - **dry-validation separates validation from models.** Complex validations (cross-field, external lookups, nested structures) have a dedicated home that's not the ActiveRecord model.
170
+ - **dry-types catch type errors at boundaries.** Instead of `NoMethodError: undefined method 'to_i' for nil` buried in a service, you get `Dry::Types::ConstraintError` at the entry point.
171
+ - **dry-monads make error flow explicit.** Every step returns `Success` or `Failure`. The `do` notation short-circuits on failure. No hidden exception paths, no nil returns.
172
+ - **Composable and standalone.** Each dry-rb gem works independently. Use dry-validation without dry-monads, or dry-types without dry-struct.
173
+
174
+ ## When To Apply
175
+
176
+ - **Complex validation beyond what ActiveModel handles.** Nested params, cross-field rules, external lookups, multi-step validation.
177
+ - **Non-Rails Ruby projects.** dry-rb is framework-agnostic. Perfect for Sinatra apps, plain Ruby services, and gems.
178
+ - **Teams that want explicit error handling.** dry-monads forces every failure path to be handled. Nothing slips through silently.
179
+ - **API input validation.** dry-validation is excellent for validating JSON API payloads before they touch ActiveRecord.
180
+
181
+ ## When NOT To Apply
182
+
183
+ - **Simple Rails apps with standard validations.** `validates :name, presence: true` is fine. Don't add dry-validation for what ActiveModel already handles.
184
+ - **The team is unfamiliar with functional patterns.** dry-monads' `Success`/`Failure`/`yield` pattern has a learning curve. If the team isn't bought in, it creates friction.
185
+ - **Mixing dry-rb and ActiveModel validations.** Pick one approach per layer. Don't validate params with dry-validation AND re-validate in the model with ActiveModel — you'll get confused about which errors come from where.
186
+ - **Small projects.** The dry-rb gems add dependencies and concepts. For a 10-controller Rails app, they're overkill.
@@ -0,0 +1,268 @@
1
+ # Gem: FactoryBot
2
+
3
+ ## What It Is
4
+
5
+ FactoryBot creates test data for RSpec and Minitest. It replaces fixtures with programmatic factories that produce valid ActiveRecord objects with minimal boilerplate. It's the most widely used test data library in Rails.
6
+
7
+ ## Setup Done Right
8
+
9
+ ```ruby
10
+ # Gemfile (test group)
11
+ gem 'factory_bot_rails'
12
+
13
+ # spec/support/factory_bot.rb
14
+ RSpec.configure do |config|
15
+ config.include FactoryBot::Syntax::Methods
16
+ end
17
+ ```
18
+
19
+ ## Gotcha #1: Factory Chains Create Way More Records Than You Think
20
+
21
+ Every `belongs_to` association in a factory triggers a `create` of the associated record. A deeply nested factory can create 10+ records when you only asked for 1.
22
+
23
+ ```ruby
24
+ # These factories look innocent...
25
+ FactoryBot.define do
26
+ factory :line_item do
27
+ order # Creates an order
28
+ product # Creates a product
29
+ quantity { 1 }
30
+ end
31
+
32
+ factory :order do
33
+ user # Creates a user
34
+ shipping_address { "123 Main St" }
35
+ end
36
+
37
+ factory :user do
38
+ company # Creates a company
39
+ email { "user@example.com" }
40
+ end
41
+
42
+ factory :company do
43
+ name { "Acme Inc" }
44
+ plan # Creates a plan
45
+ end
46
+ end
47
+
48
+ # This single line...
49
+ create(:line_item)
50
+ # Actually creates: plan → company → user → order → product → line_item
51
+ # That's 6 INSERT statements for ONE line item!
52
+ ```
53
+
54
+ **Fix: Use `build_stubbed` when you don't need the DB, and be deliberate about associations:**
55
+
56
+ ```ruby
57
+ # For unit tests — zero database hits
58
+ line_item = build_stubbed(:line_item)
59
+
60
+ # When you need a real record but want to control associations
61
+ user = create(:user, company: nil) # Skip company creation
62
+ order = create(:order, user: user) # Reuse the user
63
+ create(:line_item, order: order) # Only creates product + line_item
64
+ ```
65
+
66
+ ## Gotcha #2: Sequences vs Hardcoded Values
67
+
68
+ Unique fields without sequences cause test failures that depend on execution order.
69
+
70
+ ```ruby
71
+ # WRONG: Hardcoded unique field — second create fails
72
+ factory :user do
73
+ email { "test@example.com" } # Duplicate on second create!
74
+ end
75
+
76
+ create(:user) # Works
77
+ create(:user) # ActiveRecord::RecordNotUnique: email already taken
78
+
79
+ # RIGHT: Use sequences for any unique field
80
+ factory :user do
81
+ sequence(:email) { |n| "user#{n}@example.com" }
82
+ name { "Jane Doe" }
83
+ end
84
+
85
+ create(:user).email # "user1@example.com"
86
+ create(:user).email # "user2@example.com"
87
+ ```
88
+
89
+ **The trap:** Tests pass when run individually but fail when run together. The first test creates `test@example.com`, the second tries to create it again. Sequences prevent this.
90
+
91
+ ## Gotcha #3: `create` vs `build` vs `build_stubbed` — Wrong Choice Wastes Time
92
+
93
+ ```ruby
94
+ # WRONG: Using create when build_stubbed would work
95
+ RSpec.describe Order do
96
+ let(:order) { create(:order) } # Writes to DB unnecessarily
97
+
98
+ it "calculates total" do
99
+ # This test only calls order.total — it never queries the DB
100
+ expect(order.total).to eq(0)
101
+ end
102
+ end
103
+
104
+ # RIGHT: Match the factory strategy to the test's needs
105
+ RSpec.describe Order do
106
+ let(:order) { build_stubbed(:order, total: 100) } # No DB hit
107
+
108
+ it "calculates total" do
109
+ expect(order.total).to eq(100)
110
+ end
111
+ end
112
+ ```
113
+
114
+ Decision tree:
115
+ - **Does the test query the DB (scopes, joins, reload)?** → `create`
116
+ - **Does the test call `.valid?`, `.save`, or `.errors`?** → `build`
117
+ - **Does the test only call instance methods?** → `build_stubbed`
118
+
119
+ ## Gotcha #4: Traits — Compose, Don't Create New Factories
120
+
121
+ ```ruby
122
+ # WRONG: Separate factories for every variation
123
+ factory :pending_order do
124
+ status { :pending }
125
+ user
126
+ end
127
+
128
+ factory :shipped_order do
129
+ status { :shipped }
130
+ shipped_at { 1.day.ago }
131
+ user
132
+ end
133
+
134
+ factory :high_value_shipped_order do
135
+ status { :shipped }
136
+ shipped_at { 1.day.ago }
137
+ total { 500 }
138
+ user
139
+ end
140
+
141
+ # RIGHT: One factory with composable traits
142
+ factory :order do
143
+ user
144
+ sequence(:reference) { |n| "ORD-#{n.to_s.rjust(6, '0')}" }
145
+ status { :pending }
146
+ total { 100 }
147
+
148
+ trait :shipped do
149
+ status { :shipped }
150
+ shipped_at { 1.day.ago }
151
+ end
152
+
153
+ trait :cancelled do
154
+ status { :cancelled }
155
+ cancelled_at { 1.hour.ago }
156
+ end
157
+
158
+ trait :high_value do
159
+ total { 500 }
160
+ end
161
+
162
+ trait :with_line_items do
163
+ transient do
164
+ item_count { 2 }
165
+ end
166
+
167
+ after(:create) do |order, ctx|
168
+ create_list(:line_item, ctx.item_count, order: order)
169
+ end
170
+ end
171
+ end
172
+
173
+ # Compose traits as needed
174
+ create(:order, :shipped)
175
+ create(:order, :shipped, :high_value)
176
+ create(:order, :with_line_items, item_count: 5)
177
+ create(:order, :cancelled, total: 0)
178
+ ```
179
+
180
+ ## Gotcha #5: `after(:create)` Blocks Break `build_stubbed`
181
+
182
+ ```ruby
183
+ # WRONG: after(:create) prevents build_stubbed from working
184
+ factory :order do
185
+ after(:create) do |order|
186
+ create(:line_item, order: order) # Only runs on create, not build_stubbed
187
+ end
188
+ end
189
+
190
+ build_stubbed(:order)
191
+ # Works — but has no line items (after(:create) didn't run)
192
+ # Tests that expect line items fail silently
193
+
194
+ # RIGHT: Use traits for optional associations
195
+ factory :order do
196
+ # Base factory has no line items
197
+
198
+ trait :with_line_items do
199
+ after(:create) do |order|
200
+ create_list(:line_item, 2, order: order)
201
+ end
202
+ end
203
+ end
204
+
205
+ create(:order, :with_line_items) # Has line items
206
+ build_stubbed(:order) # No line items, but that's expected
207
+ ```
208
+
209
+ ## Gotcha #6: Faker Slows Tests and Creates Flaky Failures
210
+
211
+ ```ruby
212
+ # WRONG: Faker for every attribute
213
+ factory :user do
214
+ name { Faker::Name.name } # Random every test run
215
+ email { Faker::Internet.email } # Can generate duplicates!
216
+ bio { Faker::Lorem.paragraph(sentence_count: 10) } # Slow, nobody reads it
217
+ end
218
+
219
+ # RIGHT: Static defaults, sequences for unique fields
220
+ factory :user do
221
+ name { "Jane Doe" }
222
+ sequence(:email) { |n| "user#{n}@example.com" }
223
+ # No bio — keep it nil unless a test specifically needs it
224
+ end
225
+ ```
226
+
227
+ **The traps:**
228
+ - Faker generates random data. Your test passes 99% of the time but fails when Faker generates a name over 100 characters (your validation limit).
229
+ - Faker::Internet.email can generate the same email twice. Unlike sequences, it doesn't guarantee uniqueness.
230
+ - Faker is slow — it generates random data on every call. Across 2,000 factory calls, this adds seconds.
231
+
232
+ ## Gotcha #7: Lint Your Factories
233
+
234
+ ```ruby
235
+ # spec/factories_spec.rb — catches broken factories early
236
+ RSpec.describe "FactoryBot factories" do
237
+ it "has valid factories" do
238
+ FactoryBot.lint(traits: true)
239
+ end
240
+ end
241
+ ```
242
+
243
+ This creates every factory with every trait and calls `.valid?` on each. It catches:
244
+ - Missing required fields
245
+ - Broken associations
246
+ - Invalid trait combinations
247
+ - Sequences that produce invalid data
248
+
249
+ Run it in CI — it's the first test to fail when someone adds a model validation without updating the factory.
250
+
251
+ ## Do's and Don'ts Summary
252
+
253
+ **DO:**
254
+ - Use `build_stubbed` by default, `build` for validation tests, `create` only when the DB is needed
255
+ - Use sequences for every unique field (email, reference, slug)
256
+ - Use traits for variations — compose them, don't create separate factories
257
+ - Use transient attributes for configurable association creation
258
+ - Use static values for non-unique fields
259
+ - Lint your factories in CI
260
+ - Keep base factories minimal — only required fields
261
+
262
+ **DON'T:**
263
+ - Don't use `create` when `build_stubbed` would work
264
+ - Don't use Faker for factory defaults (slow, flaky, non-unique)
265
+ - Don't put `after(:create)` in the base factory — use traits
266
+ - Don't create separate factories for variations — use traits
267
+ - Don't forget to update factories when you add model validations
268
+ - Don't let factories silently create 10+ records via association chains