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,127 @@
1
+ # Design Pattern: Strategy
2
+
3
+ ## Pattern
4
+
5
+ Define a family of algorithms, put each in its own class, and make them interchangeable. The calling code (context) delegates to a strategy object and doesn't know or care which implementation it gets.
6
+
7
+ In Ruby, strategies can be classes, procs/lambdas, or any object that responds to the expected method — duck typing makes the pattern lightweight.
8
+
9
+ ```ruby
10
+ # Strategy as classes — best for complex algorithms with their own state
11
+
12
+ class Credits::PricingStrategy
13
+ def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+
18
+ class Credits::HaikuPricing < Credits::PricingStrategy
19
+ INPUT_RATE = 1.0 / 1_000_000 # $1 per 1M input tokens
20
+ OUTPUT_RATE = 5.0 / 1_000_000 # $5 per 1M output tokens
21
+ CACHE_RATE = 0.1 / 1_000_000 # $0.10 per 1M cached tokens
22
+
23
+ def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
24
+ (input_tokens * INPUT_RATE) +
25
+ (output_tokens * OUTPUT_RATE) +
26
+ (cache_read_tokens * CACHE_RATE)
27
+ end
28
+ end
29
+
30
+ class Credits::SonnetPricing < Credits::PricingStrategy
31
+ INPUT_RATE = 3.0 / 1_000_000
32
+ OUTPUT_RATE = 15.0 / 1_000_000
33
+ CACHE_RATE = 0.3 / 1_000_000
34
+
35
+ def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
36
+ (input_tokens * INPUT_RATE) +
37
+ (output_tokens * OUTPUT_RATE) +
38
+ (cache_read_tokens * CACHE_RATE)
39
+ end
40
+ end
41
+
42
+ # Context — doesn't know which pricing strategy it's using
43
+ class Credits::DeductionService
44
+ def initialize(pricing: Credits::HaikuPricing.new)
45
+ @pricing = pricing
46
+ end
47
+
48
+ def call(interaction)
49
+ cost = @pricing.calculate_cost(
50
+ interaction.input_tokens,
51
+ interaction.output_tokens,
52
+ interaction.cache_read_tokens
53
+ )
54
+ credits = (cost / Credits::COST_PER_CREDIT).ceil
55
+
56
+ interaction.update!(credits_used: credits, cost_usd: cost)
57
+ interaction.user.deduct_credits!(credits)
58
+ end
59
+ end
60
+ ```
61
+
62
+ Strategy as procs — best for simple, inline algorithms:
63
+
64
+ ```ruby
65
+ class Orders::SortService
66
+ STRATEGIES = {
67
+ newest: ->(scope) { scope.order(created_at: :desc) },
68
+ oldest: ->(scope) { scope.order(created_at: :asc) },
69
+ highest: ->(scope) { scope.order(total: :desc) },
70
+ alphabetical: ->(scope) { scope.joins(:user).order("users.name ASC") }
71
+ }.freeze
72
+
73
+ def call(orders, strategy_name:)
74
+ strategy = STRATEGIES.fetch(strategy_name, STRATEGIES[:newest])
75
+ strategy.call(orders)
76
+ end
77
+ end
78
+ ```
79
+
80
+ Strategy via Ruby blocks — maximum flexibility:
81
+
82
+ ```ruby
83
+ class DataExporter
84
+ def export(records, &formatter)
85
+ records.map { |record| formatter.call(record) }.join("\n")
86
+ end
87
+ end
88
+
89
+ exporter = DataExporter.new
90
+ exporter.export(orders) { |o| o.to_json }
91
+ exporter.export(orders) { |o| "#{o.reference},#{o.total}" }
92
+ ```
93
+
94
+ ## Why This Is Good
95
+
96
+ - **Adding a new algorithm doesn't touch existing code.** Adding `OpusPricing` means writing one new class. `DeductionService`, `HaikuPricing`, and `SonnetPricing` don't change.
97
+ - **Each strategy is independently testable.** Test `HaikuPricing#calculate_cost` with just numbers — no service, no user, no credits.
98
+ - **Runtime swappable.** Pro mode can use `SonnetPricing`, free tier uses `HaikuPricing`, all determined at runtime without conditionals in the service.
99
+ - **Ruby's duck typing makes it lightweight.** No interfaces to declare, no abstract classes to inherit from. Any object with `calculate_cost(input, output, cache)` is a valid strategy.
100
+
101
+ ## When To Apply
102
+
103
+ - You have **multiple algorithms for the same task** — pricing models, sorting methods, formatting options, authentication strategies.
104
+ - The algorithm **varies at runtime** based on user input, configuration, or feature flags.
105
+ - You find yourself writing `case/when` or `if/elsif` chains that select different behavior based on a type.
106
+ - You want to **test algorithms in isolation** without the context that uses them.
107
+
108
+ ## When NOT To Apply
109
+
110
+ - **Two simple branches that won't grow.** An `if premium?` / `else` is clearer than a strategy pattern for two options that are unlikely to become three.
111
+ - **The "algorithm" is a single line.** `collection.sort_by(&:name)` vs `collection.sort_by(&:created_at)` doesn't need a strategy class — just pass the sort key.
112
+ - **The behavior never varies at runtime.** If the app always uses Haiku pricing and will never use anything else, injecting a strategy adds unnecessary indirection.
113
+
114
+ ## Rails Example
115
+
116
+ ```ruby
117
+ # config/initializers/pricing.rb
118
+ PRICING_STRATEGIES = {
119
+ "haiku" => Credits::HaikuPricing.new,
120
+ "sonnet" => Credits::SonnetPricing.new,
121
+ "opus" => Credits::OpusPricing.new
122
+ }.freeze
123
+
124
+ # Used in the interaction pipeline
125
+ pricing = PRICING_STRATEGIES.fetch(interaction.model_tier, PRICING_STRATEGIES["haiku"])
126
+ Credits::DeductionService.new(pricing: pricing).call(interaction)
127
+ ```
@@ -0,0 +1,173 @@
1
+ # Design Pattern: Template Method
2
+
3
+ ## Pattern
4
+
5
+ Define the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure. The base class controls the *what* and *when*; subclasses control the *how*.
6
+
7
+ ```ruby
8
+ # Base class defines the algorithm skeleton
9
+ class Reports::BaseReport
10
+ def generate(start_date, end_date)
11
+ data = fetch_data(start_date, end_date)
12
+ filtered = apply_filters(data)
13
+ formatted = format_output(filtered)
14
+ add_metadata(formatted, start_date, end_date)
15
+ end
16
+
17
+ private
18
+
19
+ # Steps that subclasses MUST override
20
+ def fetch_data(start_date, end_date)
21
+ raise NotImplementedError, "#{self.class} must implement #fetch_data"
22
+ end
23
+
24
+ def format_output(data)
25
+ raise NotImplementedError, "#{self.class} must implement #format_output"
26
+ end
27
+
28
+ # Hook methods — subclasses CAN override, but don't have to
29
+ def apply_filters(data)
30
+ data # Default: no filtering
31
+ end
32
+
33
+ def add_metadata(output, start_date, end_date)
34
+ {
35
+ report_type: self.class.name.demodulize.underscore,
36
+ generated_at: Time.current.iso8601,
37
+ period: "#{start_date} to #{end_date}",
38
+ data: output
39
+ }
40
+ end
41
+ end
42
+
43
+ # Subclass: Revenue report
44
+ class Reports::RevenueReport < Reports::BaseReport
45
+ private
46
+
47
+ def fetch_data(start_date, end_date)
48
+ Order.where(created_at: start_date..end_date)
49
+ .group(:status)
50
+ .sum(:total)
51
+ end
52
+
53
+ def format_output(data)
54
+ data.map { |status, total| { status: status, total: total.round(2) } }
55
+ end
56
+ end
57
+
58
+ # Subclass: User activity report with custom filtering
59
+ class Reports::UserActivityReport < Reports::BaseReport
60
+ private
61
+
62
+ def fetch_data(start_date, end_date)
63
+ User.where(last_active_at: start_date..end_date)
64
+ .select(:id, :email, :last_active_at, :plan)
65
+ end
66
+
67
+ def apply_filters(data)
68
+ data.where.not(plan: "free") # Override hook: exclude free users
69
+ end
70
+
71
+ def format_output(data)
72
+ data.map { |u| { email: u.email, plan: u.plan, last_active: u.last_active_at.iso8601 } }
73
+ end
74
+ end
75
+
76
+ # Subclass: Credit usage report
77
+ class Reports::CreditUsageReport < Reports::BaseReport
78
+ private
79
+
80
+ def fetch_data(start_date, end_date)
81
+ CreditLedger.where(created_at: start_date..end_date)
82
+ .joins(:user)
83
+ .group("users.email")
84
+ .sum(:amount)
85
+ end
86
+
87
+ def format_output(data)
88
+ data.sort_by { |_, amount| amount }
89
+ .map { |email, amount| { email: email, credits_used: amount.abs } }
90
+ end
91
+ end
92
+
93
+ # Usage — all reports follow the same algorithm, different data/formatting
94
+ revenue = Reports::RevenueReport.new.generate(30.days.ago, Date.today)
95
+ activity = Reports::UserActivityReport.new.generate(7.days.ago, Date.today)
96
+ credits = Reports::CreditUsageReport.new.generate(1.month.ago.beginning_of_month, 1.month.ago.end_of_month)
97
+ ```
98
+
99
+ ## Why This Is Good
100
+
101
+ - **Algorithm is defined once.** The sequence — fetch, filter, format, add metadata — lives in `BaseReport`. No subclass can accidentally skip the metadata step or reorder the operations.
102
+ - **Variation without duplication.** Each report only implements what's different (data source, formatting). The shared steps (metadata, the overall flow) are inherited.
103
+ - **Hook methods provide optional customization.** `apply_filters` has a default (no-op). Subclasses override it only when they need filtering. No empty method stubs needed.
104
+ - **New reports are easy.** Create a new subclass, implement `fetch_data` and `format_output`, done. The algorithm skeleton works automatically.
105
+
106
+ ## Anti-Pattern
107
+
108
+ Copy-pasting the algorithm into each report class:
109
+
110
+ ```ruby
111
+ class RevenueReport
112
+ def generate(start_date, end_date)
113
+ data = Order.where(created_at: start_date..end_date).group(:status).sum(:total)
114
+ formatted = data.map { |status, total| { status: status, total: total } }
115
+ { report_type: "revenue", generated_at: Time.current.iso8601, period: "#{start_date} to #{end_date}", data: formatted }
116
+ end
117
+ end
118
+
119
+ class UserActivityReport
120
+ def generate(start_date, end_date)
121
+ data = User.where(last_active_at: start_date..end_date)
122
+ formatted = data.map { |u| { email: u.email, last_active: u.last_active_at } }
123
+ { report_type: "user_activity", generated_at: Time.current.iso8601, period: "#{start_date} to #{end_date}", data: formatted }
124
+ end
125
+ end
126
+ ```
127
+
128
+ The metadata hash is duplicated in every report. Changing the metadata format means editing every class.
129
+
130
+ ## When To Apply
131
+
132
+ - **Multiple classes follow the same algorithm with different details.** Reports, importers, exporters, notification handlers, data processors.
133
+ - **You want to enforce a sequence of steps.** The base class guarantees that filtering always happens after fetching and before formatting.
134
+ - **Common behavior + specific behavior.** Metadata generation is common. Data fetching is specific.
135
+
136
+ ## When NOT To Apply
137
+
138
+ - **Two classes with minor differences.** If only `fetch_data` varies and everything else is identical, a single class with an injected strategy (proc or data source object) is simpler than inheritance.
139
+ - **Ruby modules might be better.** If you need to mix the template into unrelated class hierarchies, use a module with a template method instead of inheritance.
140
+ - **Don't force inheritance for code reuse.** If the subclasses don't have a genuine "is-a" relationship, prefer composition (Strategy pattern) over inheritance (Template Method).
141
+
142
+ ## Edge Cases
143
+
144
+ **Template Method via modules (no inheritance needed):**
145
+
146
+ ```ruby
147
+ module Importable
148
+ def import(file_path)
149
+ rows = parse(file_path)
150
+ validated = rows.select { |row| valid?(row) }
151
+ validated.each { |row| persist(row) }
152
+ { imported: validated.size, rejected: rows.size - validated.size }
153
+ end
154
+
155
+ private
156
+
157
+ def parse(file_path) = raise(NotImplementedError)
158
+ def valid?(row) = true # Hook: override to add validation
159
+ def persist(row) = raise(NotImplementedError)
160
+ end
161
+
162
+ class CsvOrderImporter
163
+ include Importable
164
+
165
+ private
166
+
167
+ def parse(file_path) = CSV.read(file_path, headers: true).map(&:to_h)
168
+ def valid?(row) = row["total"].to_f > 0
169
+ def persist(row) = Order.create!(row)
170
+ end
171
+ ```
172
+
173
+ This avoids class inheritance while still providing the template method's algorithmic skeleton.
@@ -0,0 +1,365 @@
1
+ # Gem: Devise
2
+
3
+ ## What It Is
4
+
5
+ Devise is the standard Rails authentication gem. It handles registration, login, logout, password reset, email confirmation, account locking, and session management. It's built on Warden (Rack middleware) and provides generators, routes, views, and controllers out of the box.
6
+
7
+ ## Setup Done Right
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem 'devise'
12
+
13
+ # After bundle install
14
+ rails generate devise:install
15
+ rails generate devise User
16
+ rails db:migrate
17
+
18
+ # config/initializers/devise.rb — the settings that matter
19
+ Devise.setup do |config|
20
+ config.mailer_sender = 'noreply@rubyn.ai'
21
+
22
+ # IMPORTANT: Set these in production
23
+ config.pepper = ENV.fetch('DEVISE_PEPPER') # Extra layer on bcrypt
24
+ config.secret_key = ENV.fetch('DEVISE_SECRET_KEY') # For token generation
25
+
26
+ # Password requirements
27
+ config.password_length = 8..128
28
+ config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # Simpler, less false rejections
29
+
30
+ # Lockable — lock after failed attempts
31
+ config.lock_strategy = :failed_attempts
32
+ config.unlock_strategy = :both # Email + time
33
+ config.maximum_attempts = 5
34
+ config.unlock_in = 1.hour
35
+
36
+ # Confirmable — if you use it
37
+ config.confirm_within = 3.days
38
+ config.reconfirmable = true
39
+
40
+ # Rememberable
41
+ config.remember_for = 2.weeks
42
+ config.extend_remember_period = true # Resets timer on each visit
43
+
44
+ # Timeoutable — session timeout
45
+ config.timeout_in = 30.minutes
46
+ end
47
+ ```
48
+
49
+ ## Gotcha #1: Strong Parameters
50
+
51
+ Devise uses its own parameter sanitizer, NOT standard Rails strong params. If you add fields to the User model (like `name`), they'll be silently dropped unless you configure the sanitizer.
52
+
53
+ ```ruby
54
+ # WRONG: This does nothing for Devise actions
55
+ class UsersController < ApplicationController
56
+ def user_params
57
+ params.require(:user).permit(:email, :password, :name)
58
+ end
59
+ end
60
+
61
+ # RIGHT: Configure in ApplicationController
62
+ class ApplicationController < ActionController::Base
63
+ before_action :configure_permitted_parameters, if: :devise_controller?
64
+
65
+ protected
66
+
67
+ def configure_permitted_parameters
68
+ # sign_up: registration#create
69
+ devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :company_name])
70
+
71
+ # account_update: registration#update
72
+ devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar])
73
+
74
+ # sign_in: session#create (rarely needed)
75
+ devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
76
+ end
77
+ end
78
+ ```
79
+
80
+ **The trap:** You add a `name` field to the registration form, it submits correctly, but the name is never saved. No error, no warning — Devise silently drops unpermitted params. Check the server logs for "Unpermitted parameter: :name".
81
+
82
+ ## Gotcha #2: Customizing Controllers
83
+
84
+ When you override a Devise controller, you MUST tell the router to use your controller, AND you must call `super` or replicate Devise's internal flow correctly.
85
+
86
+ ```ruby
87
+ # Generate custom controllers
88
+ rails generate devise:controllers users -c=registrations sessions
89
+
90
+ # config/routes.rb — MUST point to your controllers
91
+ devise_for :users, controllers: {
92
+ registrations: 'users/registrations',
93
+ sessions: 'users/sessions'
94
+ }
95
+
96
+ # app/controllers/users/registrations_controller.rb
97
+ class Users::RegistrationsController < Devise::RegistrationsController
98
+ # CORRECT: Call super and add your logic around it
99
+ def create
100
+ super do |user|
101
+ # This block runs after the user is built but before redirect
102
+ if user.persisted?
103
+ Projects::CreateDefaultService.call(user)
104
+ WelcomeMailer.welcome(user).deliver_later
105
+ end
106
+ end
107
+ end
108
+
109
+ # WRONG: Completely reimplementing create without understanding Devise's flow
110
+ # def create
111
+ # @user = User.new(user_params)
112
+ # if @user.save
113
+ # redirect_to root_path
114
+ # # Missing: sign_in, flash, respond_with, location, etc.
115
+ # end
116
+ # end
117
+
118
+ protected
119
+
120
+ # Where to redirect after signup
121
+ def after_sign_up_path_for(resource)
122
+ dashboard_path
123
+ end
124
+
125
+ # Where to redirect after update
126
+ def after_update_path_for(resource)
127
+ edit_user_registration_path
128
+ end
129
+ end
130
+ ```
131
+
132
+ **The trap:** You override `create` without calling `super`. Sign-up "works" but: the user isn't signed in, the flash message is missing, the Warden session isn't set correctly, `current_user` returns nil on the next page, and Turbo/Hotwire breaks because Devise's `respond_with` isn't called.
133
+
134
+ ## Gotcha #3: `current_user` Is Nil in Unexpected Places
135
+
136
+ `current_user` relies on Warden middleware. It's not available in models, service objects, mailers, or background jobs.
137
+
138
+ ```ruby
139
+ # WRONG: current_user in a model
140
+ class Order < ApplicationRecord
141
+ before_create :set_creator
142
+ def set_creator
143
+ self.created_by = current_user # NoMethodError — models don't have current_user
144
+ end
145
+ end
146
+
147
+ # RIGHT: Use Current attributes or pass the user explicitly
148
+ class Current < ActiveSupport::CurrentAttributes
149
+ attribute :user
150
+ end
151
+
152
+ # Set in ApplicationController
153
+ class ApplicationController < ActionController::Base
154
+ before_action :set_current_user
155
+
156
+ private
157
+
158
+ def set_current_user
159
+ Current.user = current_user
160
+ end
161
+ end
162
+
163
+ # Now available everywhere in the request cycle (but NOT in background jobs)
164
+ class Order < ApplicationRecord
165
+ before_create :set_creator
166
+ def set_creator
167
+ self.created_by = Current.user&.id
168
+ end
169
+ end
170
+ ```
171
+
172
+ **The trap:** `Current.user` is request-scoped. In Sidekiq jobs, it's nil. Always pass user_id explicitly to background jobs.
173
+
174
+ ## Gotcha #4: Password Change Requires Current Password
175
+
176
+ By default, Devise requires `current_password` for any registration update. This catches people when building profile edit pages.
177
+
178
+ ```ruby
179
+ # The form MUST include current_password for updates
180
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), method: :put) do |f| %>
181
+ <%= f.text_field :name %>
182
+ <%= f.email_field :email %>
183
+
184
+ <%# THIS IS REQUIRED or the update silently fails %>
185
+ <%= f.password_field :current_password, autocomplete: "current-password" %>
186
+
187
+ <%= f.submit "Update" %>
188
+ <% end %>
189
+ ```
190
+
191
+ If you want to update profile fields WITHOUT requiring the password:
192
+
193
+ ```ruby
194
+ class Users::RegistrationsController < Devise::RegistrationsController
195
+ protected
196
+
197
+ # Allow update without password when not changing email/password
198
+ def update_resource(resource, params)
199
+ if params[:password].blank? && params[:password_confirmation].blank?
200
+ params.delete(:password)
201
+ params.delete(:password_confirmation)
202
+ params.delete(:current_password)
203
+ resource.update(params)
204
+ else
205
+ super
206
+ end
207
+ end
208
+ end
209
+ ```
210
+
211
+ ## Gotcha #5: Turbo/Hotwire Compatibility (Rails 7+)
212
+
213
+ Devise was built before Turbo. Without configuration, failed login/signup forms break because Devise returns HTTP 200 (which Turbo interprets as success) instead of 422.
214
+
215
+ ```ruby
216
+ # config/initializers/devise.rb
217
+ Devise.setup do |config|
218
+ # Rails 7+ with Turbo: Devise must return proper error status codes
219
+ config.responder.error_status = :unprocessable_entity # 422 for validation failures
220
+ config.responder.redirect_status = :see_other # 303 for redirects
221
+ end
222
+
223
+ # If you're on Devise < 4.9, you need this in ApplicationController:
224
+ class ApplicationController < ActionController::Base
225
+ class Responder < ActionController::Responder
226
+ def to_turbo_stream
227
+ controller.render(options.merge(formats: :html))
228
+ rescue ActionView::MissingTemplate => e
229
+ if get?
230
+ raise e
231
+ elsif has_errors? && default_action
232
+ render rendering_options.merge(formats: :html, status: :unprocessable_entity)
233
+ else
234
+ redirect_to navigation_location
235
+ end
236
+ end
237
+ end
238
+
239
+ self.responder = Responder
240
+ respond_to :html, :turbo_stream
241
+ end
242
+ ```
243
+
244
+ **The trap:** You submit a login form with wrong credentials. The page appears to do nothing — no error messages, no redirect. The form just sits there. The response was actually a 200 with error HTML, but Turbo expected 422 to know it should replace the form.
245
+
246
+ ## Gotcha #6: Token Authentication for APIs
247
+
248
+ Devise doesn't ship with token auth. Don't try to hack `authenticate_with_http_token` onto Devise — use a separate strategy.
249
+
250
+ ```ruby
251
+ # WRONG: Trying to use Devise for API auth
252
+ class Api::BaseController < ActionController::API
253
+ before_action :authenticate_user! # This uses session/cookie — doesn't work for APIs
254
+ end
255
+
256
+ # RIGHT: Separate API authentication
257
+ class Api::BaseController < ActionController::API
258
+ before_action :authenticate_api_key!
259
+
260
+ private
261
+
262
+ def authenticate_api_key!
263
+ token = request.headers["Authorization"]&.remove("Bearer ")
264
+ @current_user = ApiKey.find_by(key_digest: Digest::SHA256.hexdigest(token.to_s))&.user
265
+ head :unauthorized unless @current_user
266
+ end
267
+
268
+ def current_user
269
+ @current_user
270
+ end
271
+ end
272
+ ```
273
+
274
+ For JWT-based API auth, use `devise-jwt` or `doorkeeper`. Don't roll your own JWT implementation.
275
+
276
+ ## Gotcha #7: Testing with Devise
277
+
278
+ ```ruby
279
+ # spec/support/devise.rb
280
+ RSpec.configure do |config|
281
+ config.include Devise::Test::IntegrationHelpers, type: :request
282
+ config.include Devise::Test::IntegrationHelpers, type: :system
283
+ end
284
+
285
+ # In request specs — use sign_in helper
286
+ RSpec.describe "Orders", type: :request do
287
+ let(:user) { create(:user) }
288
+ before { sign_in user }
289
+
290
+ it "lists orders" do
291
+ get orders_path
292
+ expect(response).to have_http_status(:ok)
293
+ end
294
+ end
295
+
296
+ # WRONG: Trying to use sign_in in a model or service spec
297
+ # sign_in is a request/controller helper — it sets the Warden session
298
+ # In service specs, just pass the user directly
299
+
300
+ # For API specs with token auth — don't use sign_in
301
+ RSpec.describe "API Orders", type: :request do
302
+ let(:user) { create(:user) }
303
+ let(:api_key) { create(:api_key, user: user) }
304
+
305
+ it "requires auth" do
306
+ get "/api/v1/orders"
307
+ expect(response).to have_http_status(:unauthorized)
308
+ end
309
+
310
+ it "works with valid key" do
311
+ get "/api/v1/orders", headers: { "Authorization" => "Bearer #{api_key.raw_key}" }
312
+ expect(response).to have_http_status(:ok)
313
+ end
314
+ end
315
+ ```
316
+
317
+ ## Gotcha #8: Custom Mailer
318
+
319
+ Devise's default mailer sends plain text from `devise/mailer/`. To customize:
320
+
321
+ ```ruby
322
+ # Generate views first
323
+ rails generate devise:views
324
+
325
+ # For a fully custom mailer:
326
+ # config/initializers/devise.rb
327
+ config.mailer = 'CustomDeviseMailer'
328
+
329
+ # app/mailers/custom_devise_mailer.rb
330
+ class CustomDeviseMailer < Devise::Mailer
331
+ helper :application
332
+ include Devise::Controllers::UrlHelpers
333
+ layout 'mailer'
334
+
335
+ def reset_password_instructions(record, token, opts = {})
336
+ opts[:subject] = "Reset your Rubyn password"
337
+ super
338
+ end
339
+
340
+ def confirmation_instructions(record, token, opts = {})
341
+ opts[:subject] = "Confirm your Rubyn account"
342
+ super
343
+ end
344
+ end
345
+ ```
346
+
347
+ **The trap:** You create `app/views/devise/mailer/reset_password_instructions.html.erb` but emails still use the old template. Devise caches views — restart the server. If using a custom mailer class, the views should be at `app/views/custom_devise_mailer/`.
348
+
349
+ ## Do's and Don'ts Summary
350
+
351
+ **DO:**
352
+ - Set `pepper` and `secret_key` from ENV in production
353
+ - Configure parameter sanitizer for any custom fields
354
+ - Use `after_sign_up_path_for` and `after_sign_in_path_for` for redirects
355
+ - Set Turbo-compatible error/redirect status codes
356
+ - Use `sign_in` helper in request specs
357
+ - Use `Current.user` instead of threading `current_user` through every method call
358
+
359
+ **DON'T:**
360
+ - Don't override Devise controllers without calling `super` or fully understanding the flow
361
+ - Don't use Devise session auth for APIs — use token/JWT auth separately
362
+ - Don't put `current_user` in models, mailers, or jobs — it doesn't exist there
363
+ - Don't forget `current_password` in the update form
364
+ - Don't use `devise :token_authenticatable` — it was removed for security reasons
365
+ - Don't store passwords in ENV or logs — Devise handles hashing, but make sure `filter_parameters` includes `:password`