ariadna 1.3.0 → 2.0.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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/ariadna.gemspec +0 -1
  3. data/data/agents/ariadna-codebase-mapper.md +34 -722
  4. data/data/agents/ariadna-debugger.md +44 -1139
  5. data/data/agents/ariadna-executor.md +75 -396
  6. data/data/agents/ariadna-planner.md +78 -1215
  7. data/data/agents/ariadna-roadmapper.md +55 -582
  8. data/data/agents/ariadna-verifier.md +60 -702
  9. data/data/ariadna/templates/config.json +8 -33
  10. data/data/ariadna/workflows/debug.md +28 -0
  11. data/data/ariadna/workflows/execute-phase.md +31 -513
  12. data/data/ariadna/workflows/map-codebase.md +20 -319
  13. data/data/ariadna/workflows/new-milestone.md +20 -365
  14. data/data/ariadna/workflows/new-project.md +19 -880
  15. data/data/ariadna/workflows/plan-phase.md +24 -443
  16. data/data/ariadna/workflows/progress.md +20 -376
  17. data/data/ariadna/workflows/quick.md +19 -221
  18. data/data/ariadna/workflows/roadmap-ops.md +28 -0
  19. data/data/ariadna/workflows/verify-work.md +23 -560
  20. data/data/commands/ariadna/add-phase.md +11 -22
  21. data/data/commands/ariadna/debug.md +11 -143
  22. data/data/commands/ariadna/execute-phase.md +12 -30
  23. data/data/commands/ariadna/insert-phase.md +7 -14
  24. data/data/commands/ariadna/map-codebase.md +16 -49
  25. data/data/commands/ariadna/new-milestone.md +12 -25
  26. data/data/commands/ariadna/new-project.md +22 -26
  27. data/data/commands/ariadna/plan-phase.md +13 -22
  28. data/data/commands/ariadna/progress.md +16 -6
  29. data/data/commands/ariadna/quick.md +9 -11
  30. data/data/commands/ariadna/remove-phase.md +9 -12
  31. data/data/commands/ariadna/verify-work.md +14 -19
  32. data/data/skills/rails-backend/API.md +138 -0
  33. data/data/skills/rails-backend/CONTROLLERS.md +154 -0
  34. data/data/skills/rails-backend/JOBS.md +132 -0
  35. data/data/skills/rails-backend/MODELS.md +213 -0
  36. data/data/skills/rails-backend/SKILL.md +169 -0
  37. data/data/skills/rails-frontend/ASSETS.md +154 -0
  38. data/data/skills/rails-frontend/COMPONENTS.md +253 -0
  39. data/data/skills/rails-frontend/SKILL.md +187 -0
  40. data/data/skills/rails-frontend/VIEWS.md +168 -0
  41. data/data/skills/rails-performance/PROFILING.md +106 -0
  42. data/data/skills/rails-performance/SKILL.md +217 -0
  43. data/data/skills/rails-security/AUDIT.md +118 -0
  44. data/data/skills/rails-security/SKILL.md +422 -0
  45. data/data/skills/rails-testing/FIXTURES.md +78 -0
  46. data/data/skills/rails-testing/SKILL.md +160 -0
  47. data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
  48. data/lib/ariadna/installer.rb +11 -15
  49. data/lib/ariadna/tools/cli.rb +0 -12
  50. data/lib/ariadna/tools/config_manager.rb +10 -72
  51. data/lib/ariadna/tools/frontmatter.rb +23 -1
  52. data/lib/ariadna/tools/init.rb +201 -401
  53. data/lib/ariadna/tools/model_profiles.rb +6 -14
  54. data/lib/ariadna/tools/phase_manager.rb +1 -10
  55. data/lib/ariadna/tools/state_manager.rb +170 -451
  56. data/lib/ariadna/tools/template_filler.rb +4 -12
  57. data/lib/ariadna/tools/verification.rb +21 -399
  58. data/lib/ariadna/uninstaller.rb +9 -0
  59. data/lib/ariadna/version.rb +1 -1
  60. data/lib/ariadna.rb +1 -0
  61. metadata +20 -91
  62. data/data/agents/ariadna-backend-executor.md +0 -261
  63. data/data/agents/ariadna-frontend-executor.md +0 -259
  64. data/data/agents/ariadna-integration-checker.md +0 -418
  65. data/data/agents/ariadna-phase-researcher.md +0 -469
  66. data/data/agents/ariadna-plan-checker.md +0 -622
  67. data/data/agents/ariadna-project-researcher.md +0 -618
  68. data/data/agents/ariadna-research-synthesizer.md +0 -236
  69. data/data/agents/ariadna-test-executor.md +0 -266
  70. data/data/ariadna/references/checkpoints.md +0 -772
  71. data/data/ariadna/references/continuation-format.md +0 -249
  72. data/data/ariadna/references/decimal-phase-calculation.md +0 -65
  73. data/data/ariadna/references/git-integration.md +0 -248
  74. data/data/ariadna/references/git-planning-commit.md +0 -38
  75. data/data/ariadna/references/model-profile-resolution.md +0 -32
  76. data/data/ariadna/references/model-profiles.md +0 -73
  77. data/data/ariadna/references/phase-argument-parsing.md +0 -61
  78. data/data/ariadna/references/planning-config.md +0 -194
  79. data/data/ariadna/references/questioning.md +0 -153
  80. data/data/ariadna/references/rails-conventions.md +0 -416
  81. data/data/ariadna/references/tdd.md +0 -267
  82. data/data/ariadna/references/ui-brand.md +0 -160
  83. data/data/ariadna/references/verification-patterns.md +0 -853
  84. data/data/ariadna/templates/codebase/architecture.md +0 -481
  85. data/data/ariadna/templates/codebase/concerns.md +0 -380
  86. data/data/ariadna/templates/codebase/conventions.md +0 -434
  87. data/data/ariadna/templates/codebase/integrations.md +0 -328
  88. data/data/ariadna/templates/codebase/stack.md +0 -189
  89. data/data/ariadna/templates/codebase/structure.md +0 -418
  90. data/data/ariadna/templates/codebase/testing.md +0 -606
  91. data/data/ariadna/templates/context.md +0 -283
  92. data/data/ariadna/templates/continue-here.md +0 -78
  93. data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
  94. data/data/ariadna/templates/phase-prompt.md +0 -609
  95. data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
  96. data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
  97. data/data/ariadna/templates/research-project/FEATURES.md +0 -168
  98. data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
  99. data/data/ariadna/templates/research-project/STACK.md +0 -251
  100. data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
  101. data/data/ariadna/templates/state.md +0 -176
  102. data/data/ariadna/templates/summary-complex.md +0 -59
  103. data/data/ariadna/templates/summary-minimal.md +0 -41
  104. data/data/ariadna/templates/summary-standard.md +0 -48
  105. data/data/ariadna/templates/user-setup.md +0 -310
  106. data/data/ariadna/workflows/add-phase.md +0 -111
  107. data/data/ariadna/workflows/add-todo.md +0 -157
  108. data/data/ariadna/workflows/audit-milestone.md +0 -241
  109. data/data/ariadna/workflows/check-todos.md +0 -176
  110. data/data/ariadna/workflows/complete-milestone.md +0 -644
  111. data/data/ariadna/workflows/diagnose-issues.md +0 -219
  112. data/data/ariadna/workflows/discovery-phase.md +0 -289
  113. data/data/ariadna/workflows/discuss-phase.md +0 -408
  114. data/data/ariadna/workflows/execute-plan.md +0 -448
  115. data/data/ariadna/workflows/help.md +0 -470
  116. data/data/ariadna/workflows/insert-phase.md +0 -129
  117. data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
  118. data/data/ariadna/workflows/pause-work.md +0 -122
  119. data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
  120. data/data/ariadna/workflows/remove-phase.md +0 -154
  121. data/data/ariadna/workflows/research-phase.md +0 -74
  122. data/data/ariadna/workflows/resume-project.md +0 -306
  123. data/data/ariadna/workflows/set-profile.md +0 -80
  124. data/data/ariadna/workflows/settings.md +0 -145
  125. data/data/ariadna/workflows/transition.md +0 -493
  126. data/data/ariadna/workflows/update.md +0 -212
  127. data/data/ariadna/workflows/verify-phase.md +0 -226
  128. data/data/commands/ariadna/add-todo.md +0 -42
  129. data/data/commands/ariadna/audit-milestone.md +0 -42
  130. data/data/commands/ariadna/check-todos.md +0 -41
  131. data/data/commands/ariadna/complete-milestone.md +0 -136
  132. data/data/commands/ariadna/discuss-phase.md +0 -86
  133. data/data/commands/ariadna/help.md +0 -22
  134. data/data/commands/ariadna/list-phase-assumptions.md +0 -50
  135. data/data/commands/ariadna/pause-work.md +0 -35
  136. data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
  137. data/data/commands/ariadna/reapply-patches.md +0 -110
  138. data/data/commands/ariadna/research-phase.md +0 -187
  139. data/data/commands/ariadna/resume-work.md +0 -40
  140. data/data/commands/ariadna/set-profile.md +0 -34
  141. data/data/commands/ariadna/settings.md +0 -36
  142. data/data/commands/ariadna/update.md +0 -37
  143. data/data/guides/backend.md +0 -3069
  144. data/data/guides/frontend.md +0 -1479
  145. data/data/guides/performance.md +0 -1193
  146. data/data/guides/security.md +0 -1522
  147. data/data/guides/style-guide.md +0 -1091
  148. data/data/guides/testing.md +0 -504
  149. data/data/templates.md +0 -94
@@ -0,0 +1,187 @@
1
+ ---
2
+ name: rails-frontend
3
+ description: Ruby on Rails frontend conventions — Hotwire, Turbo, Stimulus, views, components, assets. Use when implementing frontend features, building views, or working with JavaScript/CSS.
4
+ ---
5
+
6
+ # Rails Frontend
7
+
8
+ Core conventions for Rails frontend work using the Hotwire stack: Turbo Drive, Turbo Frames, Turbo Streams, and Stimulus controllers.
9
+
10
+ ## Sub-files
11
+
12
+ - [VIEWS.md](VIEWS.md) — ERB conventions, layouts, partials, Turbo Frame wrapping
13
+ - [COMPONENTS.md](COMPONENTS.md) — Stimulus controllers, presenter pattern, component composition
14
+ - [ASSETS.md](ASSETS.md) — CSS architecture, design tokens, asset pipeline
15
+
16
+ ---
17
+
18
+ ## Stack
19
+
20
+ - **Turbo Drive** — replaces full page loads with fetch + DOM swap
21
+ - **Turbo Frames** — scope navigation to a region; swap on response
22
+ - **Turbo Streams** — targeted DOM mutations via `<turbo-stream>` elements
23
+ - **Stimulus** — lightweight JS controllers bound to DOM elements
24
+ - **No React/Vue** — Hotwire is the default; reach for Stimulus before any SPA framework
25
+
26
+ ---
27
+
28
+ ## Turbo Essentials
29
+
30
+ ### HTTP Status Codes
31
+
32
+ Always use these — Turbo depends on them:
33
+
34
+ | Status | Meaning | When to Use |
35
+ |--------|---------|-------------|
36
+ | `303 See Other` | Redirect after success | After any create/update/destroy |
37
+ | `422 Unprocessable Entity` | Validation failure | Re-render form in frame |
38
+ | Never `200` | For redirect-expecting forms | — |
39
+
40
+ ```ruby
41
+ def create
42
+ if @card.save
43
+ redirect_to @card, status: :see_other
44
+ else
45
+ render :new, status: :unprocessable_entity
46
+ end
47
+ end
48
+ ```
49
+
50
+ ### Turbo Frames
51
+
52
+ Scope to the **smallest rerenderable unit**. Always use `dom_id` for IDs:
53
+
54
+ ```erb
55
+ <turbo-frame id="<%= dom_id(card) %>">
56
+ <%= render "cards/card", card: card %>
57
+ </turbo-frame>
58
+ ```
59
+
60
+ Lazy loading with placeholder:
61
+
62
+ ```erb
63
+ <turbo-frame id="activity_feed" src="<%= activity_feed_path %>" loading="lazy">
64
+ <p>Loading...</p>
65
+ </turbo-frame>
66
+ ```
67
+
68
+ Style the loading state with the auto-added `[busy]` attribute:
69
+
70
+ ```css
71
+ turbo-frame[busy] { opacity: 0.5; pointer-events: none; }
72
+ ```
73
+
74
+ ### Turbo Streams
75
+
76
+ Prefer the 8 built-in actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`.
77
+
78
+ Broadcast from model callbacks:
79
+
80
+ ```ruby
81
+ class Card < ApplicationRecord
82
+ after_create_commit -> { broadcast_append_to board, target: "cards" }
83
+ after_update_commit -> { broadcast_replace_to board }
84
+ after_destroy_commit -> { broadcast_remove_to board }
85
+ end
86
+ ```
87
+
88
+ Custom stream actions register on `StreamActions`; `this` is the `<turbo-stream>` element:
89
+
90
+ ```javascript
91
+ import { StreamActions } from "@hotwired/turbo"
92
+
93
+ StreamActions.flash = function () {
94
+ const flash = document.createElement("div")
95
+ flash.className = `flash flash--${this.getAttribute("type") || "notice"}`
96
+ flash.textContent = this.getAttribute("message")
97
+ document.getElementById("flash_container").appendChild(flash)
98
+ }
99
+ ```
100
+
101
+ ### Turbo Drive Events
102
+
103
+ | Event | Use For |
104
+ |-------|---------|
105
+ | `turbo:before-cache` | Clean transient UI (close dropdowns, remove flashes) |
106
+ | `turbo:before-render` | Page transition animations — **pausable** via `preventDefault()` + `detail.resume()` |
107
+ | `turbo:load` | Post-navigation setup (equivalent to `DOMContentLoaded`) |
108
+ | `turbo:frame-load` | Frame navigation complete — update active states here, NOT on `turbo:click` |
109
+
110
+ Always guard animations against `data-turbo-preview` (cached snapshot renders):
111
+
112
+ ```javascript
113
+ document.addEventListener("turbo:before-render", (event) => {
114
+ if (document.documentElement.hasAttribute("data-turbo-preview")) return
115
+ event.preventDefault()
116
+ document.documentElement.classList.add("page-leaving")
117
+ document.documentElement.addEventListener("animationend", () => event.detail.resume(), { once: true })
118
+ })
119
+ ```
120
+
121
+ ### Optimistic UI
122
+
123
+ 1. Store markup in a `<template>` containing a `<turbo-stream>` (prevents premature execution)
124
+ 2. On `turbo:submit-start`, clone and append to the DOM
125
+ 3. Server responds with `turbo_stream.refresh` to reconcile
126
+
127
+ Use client-side ULIDs for optimistic IDs (time-ordered, collision-resistant):
128
+
129
+ ```javascript
130
+ function generateULID() {
131
+ const time = Date.now().toString(36).padStart(10, "0")
132
+ const rand = Array.from(crypto.getRandomValues(new Uint8Array(10)))
133
+ .map((b) => b.toString(36).padStart(2, "0")).join("").slice(0, 16)
134
+ return (time + rand).toUpperCase()
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Presenter Pattern
141
+
142
+ Use plain Ruby presenter classes in `app/models/` (not a separate `app/presenters/` directory) to keep view logic out of ERB templates.
143
+
144
+ **Create a presenter when:** a view needs 3+ conditionals, multiple computed values, HTML generation, or fragment caching.
145
+
146
+ Key anatomy:
147
+
148
+ ```ruby
149
+ class User::Filtering
150
+ def initialize(user, filter, expanded: false)
151
+ @user, @filter, @expanded = user, filter, expanded
152
+ end
153
+
154
+ def boards = @boards ||= user.boards.ordered_by_recently_accessed # memoized
155
+ def show_tags? = filter.tags.any? # boolean for display
156
+ def cache_key # for fragment caching
157
+ ActiveSupport::Cache.expand_cache_key([user, filter, boards], "user-filtering")
158
+ end
159
+ end
160
+ ```
161
+
162
+ - **Domain-organized names**: `User::Filtering`, not `FilteringPresenter`
163
+ - **Include ActionView helpers** when generating HTML: `include ActionView::Helpers::TagHelper`
164
+ - **Factory methods** on models: `event.description_for(user)` for discoverable APIs
165
+ - **Controller concerns** for cross-controller instantiation
166
+
167
+ ---
168
+
169
+ ## View Transitions API
170
+
171
+ Enable in the layout:
172
+
173
+ ```erb
174
+ <meta name="view-transition" content="same-origin">
175
+ ```
176
+
177
+ Direction-aware transitions: capture in `turbo:click`, apply in `turbo:before-render`, clean up in `turbo:load`.
178
+
179
+ ---
180
+
181
+ ## Key Rules
182
+
183
+ - No query calls (`where`, `find`, `count`) in ERB — push to presenters or controllers
184
+ - No conditionals deeper than one level in templates
185
+ - Always pass locals explicitly to partials — never rely on instance variables inside partials
186
+ - Use `dom_id` for all Turbo Frame and Stream target IDs
187
+ - Clean transient UI on `turbo:before-cache` (dropdowns, flash messages, open modals)
@@ -0,0 +1,168 @@
1
+ # Views — ERB, Layouts, Partials
2
+
3
+ Templates are rendering surfaces, not logic containers. Delegate decisions to presenters or model methods.
4
+
5
+ ## ERB Conventions
6
+
7
+ **Rules:**
8
+ - No conditionals deeper than one level
9
+ - No query calls (`where`, `find`, `count`) — use presenters
10
+ - Use `content_for` to inject section-specific content into layouts
11
+ - Prefer `tag.div` helpers inside presenters over inline ERB for complex HTML
12
+
13
+ ```erb
14
+ <%# Bad — logic in template %>
15
+ <% if user.avatar.attached? && user.avatar.variable? %>
16
+ <%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
17
+ <% else %>
18
+ <%= image_tag "default_avatar.png" %>
19
+ <% end %>
20
+
21
+ <%# Good — delegate to presenter %>
22
+ <%= presenter.avatar_tag %>
23
+ ```
24
+
25
+ ### `content_for` Pattern
26
+
27
+ ```erb
28
+ <%# app/views/messages/show.html.erb %>
29
+ <% content_for :title, @message.subject %>
30
+ <% content_for :head do %>
31
+ <%= javascript_include_tag "trix" %>
32
+ <% end %>
33
+ ```
34
+
35
+ ```erb
36
+ <%# app/views/layouts/application.html.erb %>
37
+ <title><%= content_for(:title) || "App" %></title>
38
+ <%= yield :head %>
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Partials
44
+
45
+ **Extract when:** the same markup appears in 2+ templates, or a clear UI component boundary exists.
46
+
47
+ **Naming:**
48
+ - Leading underscore: `_card.html.erb`
49
+ - Named after the UI concept, not the model: `_card.html.erb` not `_message_display.html.erb`
50
+ - Cross-controller partials in `app/views/shared/`
51
+
52
+ **Always pass explicit locals — never rely on instance variables inside partials:**
53
+
54
+ ```erb
55
+ <%# Good %>
56
+ <%= render partial: "messages/card", locals: { message: message, show_actions: true } %>
57
+
58
+ <%# Good — short form for collections %>
59
+ <%= render partial: "messages/message", collection: @messages, as: :message %>
60
+
61
+ <%# Bad — implicit instance variable dependency %>
62
+ <%= render partial: "messages/card" %>
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Turbo Frame Wrapping
68
+
69
+ Wrap the **smallest rerenderable unit**. Frame IDs must match between the source page and the server response.
70
+
71
+ ```erb
72
+ <%# app/views/messages/show.html.erb %>
73
+ <%= turbo_frame_tag dom_id(message) do %>
74
+ <h2><%= message.subject %></h2>
75
+ <%= link_to "Edit", edit_message_path(message) %>
76
+ <% end %>
77
+
78
+ <%# app/views/messages/edit.html.erb — same ID, swaps in place %>
79
+ <%= turbo_frame_tag dom_id(message) do %>
80
+ <%= render "form", message: message %>
81
+ <% end %>
82
+ ```
83
+
84
+ Lazy-loaded frame with placeholder:
85
+
86
+ ```erb
87
+ <%= turbo_frame_tag "comments", src: message_comments_path(message), loading: :lazy do %>
88
+ <p>Loading comments...</p>
89
+ <% end %>
90
+ ```
91
+
92
+ Tabbed navigation with history:
93
+
94
+ ```erb
95
+ <a href="<%= tab_path %>" data-turbo-frame="tab_content" data-turbo-action="advance">
96
+ Tab Name
97
+ </a>
98
+ <turbo-frame id="tab_content"><%= yield %></turbo-frame>
99
+ ```
100
+
101
+ Update active state on `turbo:frame-load` (not `turbo:click`):
102
+
103
+ ```javascript
104
+ document.addEventListener("turbo:frame-load", (event) => {
105
+ if (event.target.id !== "tab_content") return
106
+ document.querySelectorAll("[data-turbo-frame='tab_content']").forEach((link) => {
107
+ link.classList.toggle("active", link.href === event.target.src)
108
+ })
109
+ })
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Cache-Safe Views
115
+
116
+ Turbo caches pages before navigating. Transient UI reappears as stale artifacts unless cleaned up.
117
+
118
+ ```javascript
119
+ document.addEventListener("turbo:before-cache", () => {
120
+ document.querySelectorAll("[data-expanded]").forEach(el => el.removeAttribute("data-expanded"))
121
+ document.querySelectorAll(".flash").forEach(el => el.remove())
122
+ document.querySelectorAll("form").forEach(form => form.reset())
123
+ })
124
+ ```
125
+
126
+ Guard content that depends on fresh data against preview (cached) renders:
127
+
128
+ ```erb
129
+ <% unless request.headers["Purpose"] == "preview" %>
130
+ <div data-controller="polling"><%= render "metrics", stats: @stats %></div>
131
+ <% end %>
132
+ ```
133
+
134
+ Fragment caching with presenter keys:
135
+
136
+ ```erb
137
+ <% cache presenter.cache_key do %>
138
+ <%= render partial: "filters/tags", locals: { tags: presenter.tags } %>
139
+ <% end %>
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Template-Based Optimistic UI
145
+
146
+ Store `<turbo-stream>` inside a `<template>` to prevent premature execution:
147
+
148
+ ```html
149
+ <template data-optimistic-stream>
150
+ <turbo-stream action="append" target="messages">
151
+ <template>
152
+ <div class="message message--pending" id="pending_PLACEHOLDER">
153
+ <p data-placeholder>Sending...</p>
154
+ </div>
155
+ </template>
156
+ </turbo-stream>
157
+ </template>
158
+ ```
159
+
160
+ Clone and dispatch from a Stimulus controller on submit:
161
+
162
+ ```javascript
163
+ submit() {
164
+ const template = this.templateTarget.content.cloneNode(true)
165
+ template.querySelector(".message").id = `pending_${Date.now()}`
166
+ document.body.append(template)
167
+ }
168
+ ```
@@ -0,0 +1,106 @@
1
+ # Profiling & Benchmarking
2
+
3
+ Measure first, optimize second. Profile in the production Ruby version with production-sized data.
4
+
5
+ ---
6
+
7
+ ## Finding N+1 Queries in Development
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "bullet", group: :development
12
+
13
+ # config/environments/development.rb
14
+ config.after_initialize do
15
+ Bullet.enable = true
16
+ Bullet.rails_logger = true
17
+ Bullet.add_footer = true
18
+ end
19
+ ```
20
+
21
+ Check `log/bullet.log` after exercising a feature. Bullet reports N+1 alerts and unused eager loads.
22
+
23
+ Enable query log tags to trace queries to their source in staging:
24
+
25
+ ```ruby
26
+ # config/application.rb
27
+ config.active_record.query_log_tags_enabled = true
28
+ config.active_record.query_log_tags = [:controller, :action]
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Benchmarking with `benchmark-ips`
34
+
35
+ Compare two implementations before choosing one:
36
+
37
+ ```ruby
38
+ require "benchmark/ips"
39
+
40
+ Benchmark.ips do |x|
41
+ x.warmup = 2
42
+ x.time = 5
43
+ x.report("pluck") { User.where(active: true).pluck(:email) }
44
+ x.report("map") { User.where(active: true).map(&:email) }
45
+ x.compare!
46
+ end
47
+ ```
48
+
49
+ Always call `.compare!`. Always warm up. Test with representative data volume.
50
+
51
+ ---
52
+
53
+ ## Rack Mini Profiler
54
+
55
+ ```ruby
56
+ # Gemfile — development and staging only
57
+ gem "rack-mini-profiler"
58
+ gem "stackprof" # CPU flamegraphs
59
+ gem "memory_profiler" # allocation profiling
60
+ ```
61
+
62
+ Append `?pp=flamegraph` for CPU flamegraph. Use `?pp=profile-memory` for allocations.
63
+
64
+ ---
65
+
66
+ ## Query Analysis with EXPLAIN
67
+
68
+ ```ruby
69
+ # rails console
70
+ puts Order.where(user_id: 1, status: "pending").order(:created_at).explain
71
+
72
+ # PostgreSQL — actual execution stats
73
+ ActiveRecord::Base.connection.execute(
74
+ "EXPLAIN (ANALYZE, BUFFERS) #{Order.where(user_id: 1).to_sql}"
75
+ ).each { |r| puts r["QUERY PLAN"] }
76
+ ```
77
+
78
+ Warning signs: `Seq Scan` on large tables (missing index), `rows=` estimate vs actual mismatch (run `ANALYZE`), `Sort` without an index.
79
+
80
+ ---
81
+
82
+ ## Memory Profiling
83
+
84
+ ```ruby
85
+ require "memory_profiler"
86
+
87
+ report = MemoryProfiler.report { User.where(active: true).map(&:email) }
88
+ report.pretty_print(to_file: "/tmp/mem.txt")
89
+ # Review: allocated_memory_by_gem, allocated_objects_by_location
90
+ ```
91
+
92
+ Allocation reduction checklist:
93
+ - `# frozen_string_literal: true` on every file
94
+ - Extract regex to constants outside loops
95
+ - `pluck` instead of AR objects when only scalars are needed
96
+ - `User.select(:id, :email).find_each` to limit columns in batch jobs
97
+
98
+ ---
99
+
100
+ ## Review Thresholds
101
+
102
+ | Scenario | Target |
103
+ |---|---|
104
+ | Index page (dev, seeded data) | < 10 queries, < 50ms DB time |
105
+ | Show page | < 5 queries, < 20ms DB time |
106
+ | Batch job over 10k records | Uses `find_each`, < 256MB peak RSS |
@@ -0,0 +1,217 @@
1
+ ---
2
+ name: rails-performance
3
+ description: Ruby on Rails performance conventions — N+1 prevention, caching, database tuning, benchmarking. Use when optimizing queries, adding caching, or profiling performance.
4
+ ---
5
+
6
+ # Rails Performance Skill
7
+
8
+ Opinionated conventions for Rails performance. Every pattern has a clear unsafe anti-pattern and safe fix.
9
+
10
+ **Sub-files:**
11
+ - [PROFILING.md](PROFILING.md) — Profiling, benchmarking, and measurement workflows
12
+
13
+ ---
14
+
15
+ ## N+1 Query Prevention
16
+
17
+ Always eager load associations accessed in loops:
18
+
19
+ ```ruby
20
+ @posts = Post.includes(:author) # single association
21
+ @post = Post.includes(comments: :author).find(id) # nested
22
+ ```
23
+
24
+ Counter caches instead of `.count` in loops:
25
+
26
+ ```ruby
27
+ # Model: belongs_to :board, counter_cache: true
28
+ # Migration: add_column :boards, :cards_count, :integer, default: 0, null: false
29
+ board.cards_count # reads column — no query
30
+ board.cards.count # fires COUNT — N+1 in loops
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Efficient Queries
36
+
37
+ ```ruby
38
+ # pluck returns plain arrays — no AR objects allocated
39
+ emails = User.where(active: true).pluck(:email)
40
+ ids = Order.where(status: "pending").ids
41
+
42
+ # exists? → SELECT 1 LIMIT 1 (not .present?, .any?, .count > 0)
43
+ user.orders.where(status: "pending").exists?
44
+
45
+ # SQL aggregation — never load records to compute in Ruby
46
+ User.order(created_at: :desc) # NOT .all.sort_by(&:created_at).reverse
47
+ Order.distinct.pluck(:status) # NOT .all.map(&:status).uniq
48
+ Order.sum(:total_price) # NOT .all.sum(&:total_price)
49
+ Product.group(:category_id).count # preload all counts in one query
50
+ ```
51
+
52
+ **Batch processing for large result sets:**
53
+
54
+ ```ruby
55
+ User.find_each { |u| process(u) } # 1000 at a time
56
+ Order.where("created_at < ?", 1.year.ago).find_each(&:archive!)
57
+ Product.where(discontinued: true).find_in_batches(batch_size: 100) do |batch|
58
+ Index.bulk_delete(batch)
59
+ end
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Database Indexing
65
+
66
+ Index every FK column and every column used in `where`, `order`, `find_by`:
67
+
68
+ ```ruby
69
+ # t.references adds index by default
70
+ create_table :comments do |t|
71
+ t.references :post, foreign_key: true
72
+ t.references :user, foreign_key: true
73
+ end
74
+
75
+ add_index :articles, :status
76
+ add_index :articles, :slug, unique: true
77
+ add_index :users, :email, where: "active = true", unique: true # partial
78
+
79
+ # Composite for multi-column queries
80
+ add_index :orders, [:user_id, :status, :created_at]
81
+ ```
82
+
83
+ Concurrent index creation on live tables:
84
+
85
+ ```ruby
86
+ class AddIndexToOrdersStatus < ActiveRecord::Migration[7.1]
87
+ disable_ddl_transaction!
88
+ def change
89
+ add_index :orders, :status, algorithm: :concurrently
90
+ end
91
+ end
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Caching
97
+
98
+ **Production cache store — Redis, Memcached, or Solid Cache. Never `:file_store` or `:memory_store`:**
99
+
100
+ ```ruby
101
+ # config/environments/production.rb
102
+ config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL"), expires_in: 1.hour }
103
+ ```
104
+
105
+ **Fragment caching with auto cache-busting via `cache_key_with_version`:**
106
+
107
+ ```erb
108
+ <% cache(@project) do %>
109
+ <%= render "projects/detail", project: @project %>
110
+ <% end %>
111
+
112
+ <%# Collection multi-fetch — one Redis round-trip %>
113
+ <%= render partial: "products/product", collection: @products, cached: true %>
114
+ ```
115
+
116
+ **Application-level caching and per-request memoization:**
117
+
118
+ ```ruby
119
+ def stats
120
+ Rails.cache.fetch("dashboard/stats", expires_in: 15.minutes) do
121
+ { revenue: Order.sum(:total), active: User.where("last_sign_in_at > ?", 30.days.ago).count }
122
+ end
123
+ end
124
+
125
+ def active_subscription
126
+ @active_subscription ||= subscriptions.where(active: true).order(created_at: :desc).first
127
+ end
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Memory Management
133
+
134
+ Stream large exports — never build in memory:
135
+
136
+ ```ruby
137
+ def export
138
+ headers["Content-Disposition"] = 'attachment; filename="users.csv"'
139
+ headers["Content-Type"] = "text/csv"
140
+ response.status = 200
141
+ self.response_body = Enumerator.new do |y|
142
+ y << CSV.generate_line(["Name", "Email"])
143
+ User.find_each { |u| y << CSV.generate_line([u.name, u.email]) }
144
+ end
145
+ end
146
+ ```
147
+
148
+ Always use `deliver_later` and background jobs for slow work:
149
+
150
+ ```ruby
151
+ OrderMailer.confirmation(@order).deliver_later # NOT deliver_now
152
+ GenerateInvoiceJob.perform_later(@order) # NOT inline PDF generation
153
+ ```
154
+
155
+ Freeze string literals; extract regex constants out of loops:
156
+
157
+ ```ruby
158
+ # frozen_string_literal: true
159
+ CAPITAL = /\A[A-Z]/
160
+ users.each { |u| u.name.match?(CAPITAL) }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## View & Response
166
+
167
+ ```erb
168
+ <%# Single render call with collection caching %>
169
+ <%= render partial: "products/product", collection: @products, cached: true %>
170
+
171
+ <%# Lazy-load expensive sections %>
172
+ <%= turbo_frame_tag "stats", src: dashboard_stats_path, loading: :lazy do %>
173
+ <p>Loading...</p>
174
+ <% end %>
175
+ ```
176
+
177
+ Always paginate index actions:
178
+
179
+ ```ruby
180
+ def index
181
+ @orders = Order.order(created_at: :desc).page(params[:page]).per(25)
182
+ end
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Production Configuration
188
+
189
+ ```ruby
190
+ # config/puma.rb
191
+ threads ENV.fetch("RAILS_MIN_THREADS", 5).to_i, ENV.fetch("RAILS_MAX_THREADS", 5).to_i
192
+ workers ENV.fetch("WEB_CONCURRENCY", 2).to_i
193
+ preload_app!
194
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
195
+
196
+ # config/environments/production.rb
197
+ config.cache_classes = true
198
+ config.eager_load = true
199
+ config.assets.compile = false
200
+ config.assets.digest = true
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Check-to-File Mapping
206
+
207
+ | Changed files | Priority checks |
208
+ |---|---|
209
+ | `app/controllers/**/*.rb` | N+1 includes, `exists?`, pagination, `deliver_later` |
210
+ | `app/models/**/*.rb` | `pluck` vs `map`, SQL aggregation, memoization |
211
+ | `app/views/**/*.erb` | Fragment caching, `render collection:`, lazy frames |
212
+ | `app/jobs/**/*.rb` | `find_each`, `find_in_batches`, memory |
213
+ | `db/migrate/**/*.rb` | FK indexes, concurrent index, composite indexes |
214
+ | `config/environments/production.rb` | Cache store, `eager_load`, assets |
215
+ | `config/puma.rb` | Threads/workers, YJIT |
216
+
217
+ See [PROFILING.md](PROFILING.md) for measurement, benchmarking, and query analysis.