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,168 @@
1
+ # Rails: Form Objects
2
+
3
+ ## Pattern
4
+
5
+ Use form objects when a form doesn't map cleanly to a single ActiveRecord model. Form objects encapsulate validation, data transformation, and multi-model persistence behind an ActiveModel-compliant interface that works with Rails form helpers.
6
+
7
+ ```ruby
8
+ # app/forms/registration_form.rb
9
+ class RegistrationForm
10
+ include ActiveModel::Model
11
+ include ActiveModel::Attributes
12
+
13
+ attribute :email, :string
14
+ attribute :password, :string
15
+ attribute :password_confirmation, :string
16
+ attribute :company_name, :string
17
+ attribute :plan, :string, default: "free"
18
+ attribute :terms_accepted, :boolean
19
+
20
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
21
+ validates :password, presence: true, length: { minimum: 8 }
22
+ validates :password_confirmation, presence: true
23
+ validates :company_name, presence: true
24
+ validates :terms_accepted, acceptance: true
25
+ validate :passwords_match
26
+ validate :email_not_taken
27
+
28
+ def save
29
+ return false unless valid?
30
+
31
+ ActiveRecord::Base.transaction do
32
+ company = Company.create!(name: company_name, plan: plan)
33
+ user = company.users.create!(email: email, password: password, role: :admin)
34
+ Onboarding::WelcomeService.call(user)
35
+ end
36
+
37
+ true
38
+ rescue ActiveRecord::RecordInvalid => e
39
+ errors.add(:base, e.message)
40
+ false
41
+ end
42
+
43
+ private
44
+
45
+ def passwords_match
46
+ errors.add(:password_confirmation, "doesn't match") unless password == password_confirmation
47
+ end
48
+
49
+ def email_not_taken
50
+ errors.add(:email, "is already registered") if User.exists?(email: email)
51
+ end
52
+ end
53
+ ```
54
+
55
+ The controller stays thin:
56
+
57
+ ```ruby
58
+ # app/controllers/registrations_controller.rb
59
+ class RegistrationsController < ApplicationController
60
+ def new
61
+ @form = RegistrationForm.new
62
+ end
63
+
64
+ def create
65
+ @form = RegistrationForm.new(registration_params)
66
+
67
+ if @form.save
68
+ redirect_to dashboard_path, notice: "Welcome!"
69
+ else
70
+ render :new, status: :unprocessable_entity
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def registration_params
77
+ params.require(:registration_form).permit(:email, :password, :password_confirmation, :company_name, :plan, :terms_accepted)
78
+ end
79
+ end
80
+ ```
81
+
82
+ The view works with standard form helpers:
83
+
84
+ ```erb
85
+ <%= form_with model: @form, url: registrations_path do |f| %>
86
+ <%= f.text_field :email %>
87
+ <%= f.password_field :password %>
88
+ <%= f.password_field :password_confirmation %>
89
+ <%= f.text_field :company_name %>
90
+ <%= f.check_box :terms_accepted %>
91
+ <%= f.submit "Sign Up" %>
92
+ <% end %>
93
+ ```
94
+
95
+ ## Why This Is Good
96
+
97
+ - **Validates as a unit.** Cross-field validations (password confirmation, terms acceptance) and cross-model checks (email uniqueness) live together in one object rather than scattered across multiple models.
98
+ - **Works with Rails forms.** Including `ActiveModel::Model` gives you `form_with` compatibility, error messages, and all the form helpers for free.
99
+ - **Keeps models clean.** The User model doesn't need a `terms_accepted` virtual attribute or a `password_confirmation` validation that only applies during registration.
100
+ - **Testable.** Instantiate the form with params, call `.save`, assert results. No HTTP, no controllers, no views.
101
+ - **Transactional.** Multi-model persistence wraps in a transaction naturally within the `save` method.
102
+
103
+ ## Anti-Pattern
104
+
105
+ Stuffing virtual attributes and context-specific validations into the model:
106
+
107
+ ```ruby
108
+ # app/models/user.rb
109
+ class User < ApplicationRecord
110
+ attr_accessor :company_name, :plan, :terms_accepted, :registering
111
+
112
+ validates :terms_accepted, acceptance: true, if: :registering
113
+ validates :password_confirmation, presence: true, if: :registering
114
+ validates :company_name, presence: true, if: :registering
115
+
116
+ after_create :create_company_and_onboard, if: :registering
117
+
118
+ private
119
+
120
+ def create_company_and_onboard
121
+ company = Company.create!(name: company_name, plan: plan)
122
+ self.update!(company: company, role: :admin)
123
+ Onboarding::WelcomeService.call(self)
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Why This Is Bad
129
+
130
+ - **Conditional validations pollute the model.** Every `if: :registering` is a code smell. The model accumulates flags and conditionals for every context it's used in (registration, profile update, admin edit, API creation).
131
+ - **Virtual attributes bloat the model.** `company_name`, `plan`, `terms_accepted` have nothing to do with the User model — they're registration-specific concerns.
132
+ - **Callbacks hide side effects.** `after_create :create_company_and_onboard` runs silently whenever a user is created with the `registering` flag. Creating a user in the console, a seed file, or a test unexpectedly triggers company creation if someone accidentally sets the flag.
133
+ - **Hard to test.** Testing registration requires setting `user.registering = true` and knowing about the hidden callback chain. The test is coupled to implementation details.
134
+
135
+ ## When To Apply
136
+
137
+ Use a form object when ANY of these are true:
138
+
139
+ - The form spans **multiple models** (registration creates a user AND a company)
140
+ - The form has **virtual attributes** that don't exist on any model (terms acceptance, password confirmation for non-Devise setups, promotional codes)
141
+ - **Validations are context-specific** — they apply during this form submission but not when the model is used elsewhere
142
+ - The form requires **data transformation** before persistence (parsing dates, splitting full name into first/last, geocoding an address)
143
+ - The form has **complex conditional logic** about which fields are required based on other field values
144
+
145
+ ## When NOT To Apply
146
+
147
+ - The form maps **directly to one model** with no virtual attributes and no context-specific validations. Use the model directly — a form object adds a pointless layer.
148
+ - The form is **read-only** (search, filter). Use a simple parameter object or a query object instead.
149
+ - The **only difference** from the model is one extra validation. Add it to the model with a context (`validates :field, presence: true, on: :registration`) rather than creating an entire form object for one rule.
150
+
151
+ ## Edge Cases
152
+
153
+ **The form needs to update existing records, not just create:**
154
+ Add a constructor that accepts an existing record and populates attributes from it. The `save` method checks for `persisted?` and calls `update!` instead of `create!`.
155
+
156
+ ```ruby
157
+ def initialize(user: nil, **attributes)
158
+ @user = user
159
+ super(**attributes)
160
+ self.email ||= @user&.email
161
+ end
162
+ ```
163
+
164
+ **The form has nested attributes (like line items on an order):**
165
+ Form objects can include their own nested form objects or accept arrays. This is where form objects really shine over `accepts_nested_attributes_for`, which is brittle and hard to validate.
166
+
167
+ **The team uses the `reform` or `dry-validation` gem:**
168
+ Follow the team's existing pattern. If they use Reform, write a Reform form. Rubyn adapts to the project's conventions.
@@ -0,0 +1,229 @@
1
+ # Rails: Hotwire (Turbo + Stimulus)
2
+
3
+ ## Pattern
4
+
5
+ Hotwire delivers fast, reactive UIs by sending HTML over the wire instead of JSON. Turbo handles navigation and page updates without JavaScript. Stimulus adds sprinkles of JS behavior when needed. Together, they replace most SPA complexity.
6
+
7
+ ### Turbo Drive (Automatic)
8
+
9
+ ```ruby
10
+ # Turbo Drive is automatic — every link click and form submission
11
+ # is intercepted and fetched via fetch(), replacing the body.
12
+ # No configuration needed. Just use standard Rails link and form helpers.
13
+
14
+ # The key contract: return proper HTTP status codes
15
+ class OrdersController < ApplicationController
16
+ def create
17
+ @order = current_user.orders.build(order_params)
18
+
19
+ if @order.save
20
+ redirect_to @order, notice: "Order created." # 303 redirect → Turbo follows it
21
+ else
22
+ render :new, status: :unprocessable_entity # 422 → Turbo replaces the page
23
+ end
24
+ end
25
+
26
+ def update
27
+ @order = current_user.orders.find(params[:id])
28
+
29
+ if @order.update(order_params)
30
+ redirect_to @order, notice: "Updated."
31
+ else
32
+ render :edit, status: :unprocessable_entity # MUST return 422 for Turbo to re-render
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ ### Turbo Frames (Partial Page Updates)
39
+
40
+ ```erb
41
+ <%# app/views/orders/index.html.erb %>
42
+ <%# Only the content inside the turbo_frame is replaced on navigation %>
43
+ <h1>Orders</h1>
44
+
45
+ <%= turbo_frame_tag "orders_list" do %>
46
+ <%= render @orders %>
47
+
48
+ <%# Pagination links inside the frame only update the frame %>
49
+ <%= paginate @orders %>
50
+ <% end %>
51
+
52
+ <%# Search form targets the frame %>
53
+ <%= form_with url: orders_path, method: :get, data: { turbo_frame: "orders_list" } do |f| %>
54
+ <%= f.search_field :q, placeholder: "Search orders..." %>
55
+ <%= f.submit "Search" %>
56
+ <% end %>
57
+ ```
58
+
59
+ ```erb
60
+ <%# app/views/orders/_order.html.erb %>
61
+ <%= turbo_frame_tag dom_id(order) do %>
62
+ <div class="order-card">
63
+ <h3><%= order.reference %></h3>
64
+ <p><%= order.status %></p>
65
+ <%= link_to "Edit", edit_order_path(order) %>
66
+ </div>
67
+ <% end %>
68
+
69
+ <%# Clicking "Edit" loads the edit form INTO the frame — no full page load %>
70
+ ```
71
+
72
+ ```erb
73
+ <%# app/views/orders/edit.html.erb %>
74
+ <%= turbo_frame_tag dom_id(@order) do %>
75
+ <%= render "form", order: @order %>
76
+ <% end %>
77
+ ```
78
+
79
+ ### Turbo Streams (Real-Time Updates)
80
+
81
+ ```ruby
82
+ # After creating an order, broadcast updates to the page
83
+ class Order < ApplicationRecord
84
+ after_create_commit -> {
85
+ broadcast_prepend_to "orders",
86
+ target: "orders_list",
87
+ partial: "orders/order",
88
+ locals: { order: self }
89
+ }
90
+
91
+ after_update_commit -> {
92
+ broadcast_replace_to "orders",
93
+ target: dom_id(self),
94
+ partial: "orders/order",
95
+ locals: { order: self }
96
+ }
97
+
98
+ after_destroy_commit -> {
99
+ broadcast_remove_to "orders", target: dom_id(self)
100
+ }
101
+ end
102
+ ```
103
+
104
+ ```erb
105
+ <%# app/views/orders/index.html.erb %>
106
+ <%= turbo_stream_from "orders" %>
107
+
108
+ <div id="orders_list">
109
+ <%= render @orders %>
110
+ </div>
111
+ ```
112
+
113
+ Inline Turbo Stream responses from controller actions:
114
+
115
+ ```ruby
116
+ class OrdersController < ApplicationController
117
+ def create
118
+ @order = current_user.orders.build(order_params)
119
+
120
+ if @order.save
121
+ respond_to do |format|
122
+ format.turbo_stream {
123
+ render turbo_stream: turbo_stream.prepend("orders_list",
124
+ partial: "orders/order", locals: { order: @order })
125
+ }
126
+ format.html { redirect_to orders_path }
127
+ end
128
+ else
129
+ render :new, status: :unprocessable_entity
130
+ end
131
+ end
132
+
133
+ def destroy
134
+ @order = current_user.orders.find(params[:id])
135
+ @order.destroy!
136
+
137
+ respond_to do |format|
138
+ format.turbo_stream { render turbo_stream: turbo_stream.remove(dom_id(@order)) }
139
+ format.html { redirect_to orders_path }
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### Stimulus (Sprinkles of JavaScript)
146
+
147
+ ```javascript
148
+ // app/javascript/controllers/toggle_controller.js
149
+ import { Controller } from "@hotwired/stimulus"
150
+
151
+ export default class extends Controller {
152
+ static targets = ["content"]
153
+
154
+ toggle() {
155
+ this.contentTarget.classList.toggle("hidden")
156
+ }
157
+ }
158
+ ```
159
+
160
+ ```erb
161
+ <%# Usage in HTML — no inline JS, no jQuery %>
162
+ <div data-controller="toggle">
163
+ <button data-action="click->toggle#toggle">Show Details</button>
164
+ <div data-toggle-target="content" class="hidden">
165
+ <p>Order details here...</p>
166
+ </div>
167
+ </div>
168
+ ```
169
+
170
+ ```javascript
171
+ // app/javascript/controllers/auto_submit_controller.js
172
+ import { Controller } from "@hotwired/stimulus"
173
+
174
+ export default class extends Controller {
175
+ static targets = ["form"]
176
+
177
+ submit() {
178
+ clearTimeout(this.timeout)
179
+ this.timeout = setTimeout(() => {
180
+ this.formTarget.requestSubmit()
181
+ }, 300)
182
+ }
183
+ }
184
+ ```
185
+
186
+ ```erb
187
+ <%= form_with url: orders_path, method: :get,
188
+ data: { controller: "auto-submit", auto_submit_target: "form", turbo_frame: "orders_list" } do |f| %>
189
+ <%= f.search_field :q, data: { action: "input->auto-submit#submit" }, placeholder: "Search..." %>
190
+ <% end %>
191
+ ```
192
+
193
+ ## Why This Is Good
194
+
195
+ - **No JavaScript framework needed.** Turbo + Stimulus replaces 90% of what React/Vue do for typical CRUD apps, with far less code.
196
+ - **Server-rendered HTML.** No JSON API, no serializers, no client-side state management. The server renders HTML and Turbo delivers it to the right place.
197
+ - **Progressive enhancement.** Everything works without JavaScript (Turbo Drive degrades gracefully). Stimulus adds interactivity on top.
198
+ - **Turbo Streams enable real-time.** WebSocket-powered live updates without a single line of custom JavaScript. New orders appear instantly for all users.
199
+ - **Stimulus controllers are tiny.** 10-20 lines each, reusable across views, no build step complexity.
200
+
201
+ ## Anti-Pattern
202
+
203
+ Disabling Turbo or fighting it:
204
+
205
+ ```erb
206
+ <%# BAD: Disabling Turbo because forms don't work %>
207
+ <%= form_with model: @order, data: { turbo: false } do |f| %>
208
+
209
+ <%# The real fix: return the correct status code %>
210
+ def create
211
+ if @order.save
212
+ redirect_to @order # 303 — Turbo follows
213
+ else
214
+ render :new, status: :unprocessable_entity # 422 — Turbo re-renders
215
+ # NOT: render :new (200 status makes Turbo think it succeeded)
216
+ end
217
+ end
218
+ ```
219
+
220
+ ## When To Apply
221
+
222
+ - **Every new Rails 7+ app.** Hotwire is the default. Use it.
223
+ - **CRUD-heavy apps.** Forms, lists, search, pagination, inline editing — Hotwire handles all of these with minimal JavaScript.
224
+ - **Real-time features.** Chat, notifications, live dashboards — Turbo Streams over WebSockets.
225
+
226
+ ## When NOT To Apply
227
+
228
+ - **Complex client-side interactions.** Drag-and-drop editors, canvas drawing, real-time collaboration — these may need a JavaScript framework.
229
+ - **Offline-first apps.** Turbo requires a server connection. PWAs with offline support need client-side state.
@@ -0,0 +1,192 @@
1
+ # Rails: Internationalization (i18n)
2
+
3
+ ## Pattern
4
+
5
+ Use Rails i18n for all user-facing text, even in English-only apps. It centralizes copy, supports pluralization, enables future translation, and keeps views clean.
6
+
7
+ ### Basic Setup
8
+
9
+ ```yaml
10
+ # config/locales/en.yml
11
+ en:
12
+ orders:
13
+ index:
14
+ title: "Your Orders"
15
+ empty: "You haven't placed any orders yet."
16
+ count:
17
+ one: "%{count} order"
18
+ other: "%{count} orders"
19
+ show:
20
+ title: "Order %{reference}"
21
+ status:
22
+ pending: "Awaiting confirmation"
23
+ confirmed: "Processing"
24
+ shipped: "On its way"
25
+ delivered: "Delivered"
26
+ cancelled: "Cancelled"
27
+ create:
28
+ success: "Order placed successfully!"
29
+ failure: "Could not place order. Please check the errors below."
30
+
31
+ shared:
32
+ actions:
33
+ edit: "Edit"
34
+ delete: "Delete"
35
+ cancel: "Cancel"
36
+ save: "Save Changes"
37
+ back: "Back"
38
+ confirmations:
39
+ delete: "Are you sure? This cannot be undone."
40
+ ```
41
+
42
+ ### Usage in Views
43
+
44
+ ```erb
45
+ <%# Views use t() helper %>
46
+ <h1><%= t(".title") %></h1> <%# Lazy lookup — resolves to orders.index.title %>
47
+
48
+ <% if @orders.empty? %>
49
+ <p><%= t(".empty") %></p>
50
+ <% else %>
51
+ <p><%= t(".count", count: @orders.count) %></p> <%# Pluralization %>
52
+ <% end %>
53
+
54
+ <%= link_to t("shared.actions.edit"), edit_order_path(@order) %>
55
+
56
+ <%# Status with translation %>
57
+ <span class="badge"><%= t("orders.show.status.#{@order.status}") %></span>
58
+ ```
59
+
60
+ ### Usage in Controllers
61
+
62
+ ```ruby
63
+ class OrdersController < ApplicationController
64
+ def create
65
+ @order = current_user.orders.build(order_params)
66
+ if @order.save
67
+ redirect_to @order, notice: t(".success")
68
+ else
69
+ flash.now[:alert] = t(".failure")
70
+ render :new, status: :unprocessable_entity
71
+ end
72
+ end
73
+
74
+ def destroy
75
+ @order.destroy
76
+ redirect_to orders_path, notice: t("orders.destroy.success", reference: @order.reference)
77
+ end
78
+ end
79
+ ```
80
+
81
+ ### Model Validations
82
+
83
+ ```yaml
84
+ # config/locales/en.yml
85
+ en:
86
+ activerecord:
87
+ errors:
88
+ models:
89
+ order:
90
+ attributes:
91
+ shipping_address:
92
+ blank: "is required for delivery"
93
+ total:
94
+ greater_than: "must be a positive amount"
95
+ user:
96
+ attributes:
97
+ email:
98
+ taken: "is already registered. Did you mean to sign in?"
99
+ ```
100
+
101
+ ```ruby
102
+ # These override Rails' default validation messages automatically
103
+ class Order < ApplicationRecord
104
+ validates :shipping_address, presence: true
105
+ # Error message: "Shipping address is required for delivery"
106
+ end
107
+ ```
108
+
109
+ ### Organizing Locale Files
110
+
111
+ ```
112
+ config/locales/
113
+ ├── en.yml # Shared/global translations
114
+ ├── models/
115
+ │ ├── en.yml # ActiveRecord model names and attributes
116
+ │ └── errors/
117
+ │ └── en.yml # Validation error messages
118
+ ├── views/
119
+ │ ├── orders.en.yml # Order view translations
120
+ │ ├── users.en.yml # User view translations
121
+ │ └── admin.en.yml # Admin panel translations
122
+ └── mailers/
123
+ └── en.yml # Email subject lines and content
124
+ ```
125
+
126
+ ```ruby
127
+ # config/application.rb
128
+ config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.yml")]
129
+ config.i18n.default_locale = :en
130
+ config.i18n.fallbacks = true # Fall back to :en if translation missing
131
+ ```
132
+
133
+ ### Date and Number Formatting
134
+
135
+ ```yaml
136
+ # config/locales/en.yml
137
+ en:
138
+ date:
139
+ formats:
140
+ short: "%b %d" # "Mar 20"
141
+ long: "%B %d, %Y" # "March 20, 2026"
142
+ time:
143
+ formats:
144
+ short: "%b %d, %I:%M %p" # "Mar 20, 02:30 PM"
145
+ number:
146
+ currency:
147
+ format:
148
+ unit: "$"
149
+ precision: 2
150
+ ```
151
+
152
+ ```erb
153
+ <%= l(@order.created_at, format: :long) %> <%# March 20, 2026 %>
154
+ <%= l(@order.created_at, format: :short) %> <%# Mar 20 %>
155
+ <%= number_to_currency(@order.total / 100.0) %> <%# $25.00 %>
156
+ ```
157
+
158
+ ## Why This Is Good
159
+
160
+ - **Single source of truth for copy.** Changing "Place Order" to "Complete Purchase" across the entire app means editing one YAML line, not grep-replacing across 15 files.
161
+ - **Lazy lookup keeps views clean.** `t(".title")` in `orders/index.html.erb` automatically resolves to `en.orders.index.title`. No long key paths in views.
162
+ - **Pluralization is handled.** `t(".count", count: 1)` → "1 order." `t(".count", count: 5)` → "5 orders." Works correctly for languages with complex pluralization rules.
163
+ - **Validation messages are customizable per model.** "Email is already registered. Did you mean to sign in?" is more helpful than "Email has already been taken."
164
+ - **Future-proofs for translation.** Even if you're English-only today, adding Spanish later means adding `es.yml` files — no code changes.
165
+
166
+ ## Anti-Pattern
167
+
168
+ ```ruby
169
+ # BAD: Hardcoded strings in views
170
+ <h1>Your Orders</h1>
171
+ <p>You have <%= @orders.count %> order<%= @orders.count == 1 ? "" : "s" %></p>
172
+
173
+ # BAD: Hardcoded strings in controllers
174
+ redirect_to @order, notice: "Order placed successfully!"
175
+ flash[:alert] = "Something went wrong"
176
+
177
+ # BAD: Hardcoded validation messages
178
+ validates :email, uniqueness: { message: "is already taken" }
179
+ # Use locale files instead — they're overridable per model
180
+ ```
181
+
182
+ ## When To Apply
183
+
184
+ - **Every user-facing string.** Views, flash messages, mailer subject lines, validation messages, error pages.
185
+ - **Even English-only apps.** Centralizing copy in YAML is valuable for consistency and maintainability regardless of language count.
186
+ - **Date and number formatting.** Use `l()` for dates and `number_to_currency` for money — they respect locale settings.
187
+
188
+ ## When NOT To Apply
189
+
190
+ - **Log messages.** Logs are for developers, not users. Log in English, always.
191
+ - **Developer-facing text.** Rake task output, console messages, internal error classes. These stay as plain strings.
192
+ - **API responses.** JSON APIs typically return machine-readable codes, not translated text. Error codes like `"insufficient_credits"` don't need i18n.