ag-cortex 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.
- package/.agent/commands/test-browser.md +339 -0
- package/.agent/rules/00-constitution.md +46 -0
- package/.agent/rules/project-rules.md +49 -0
- package/.agent/skills/agent-browser/SKILL.md +223 -0
- package/.agent/skills/agent-native-architecture/SKILL.md +435 -0
- package/.agent/skills/agent-native-architecture/references/action-parity-discipline.md +409 -0
- package/.agent/skills/agent-native-architecture/references/agent-execution-patterns.md +467 -0
- package/.agent/skills/agent-native-architecture/references/agent-native-testing.md +582 -0
- package/.agent/skills/agent-native-architecture/references/architecture-patterns.md +478 -0
- package/.agent/skills/agent-native-architecture/references/dynamic-context-injection.md +338 -0
- package/.agent/skills/agent-native-architecture/references/files-universal-interface.md +301 -0
- package/.agent/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +359 -0
- package/.agent/skills/agent-native-architecture/references/mcp-tool-design.md +506 -0
- package/.agent/skills/agent-native-architecture/references/mobile-patterns.md +871 -0
- package/.agent/skills/agent-native-architecture/references/product-implications.md +443 -0
- package/.agent/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +317 -0
- package/.agent/skills/agent-native-architecture/references/self-modification.md +269 -0
- package/.agent/skills/agent-native-architecture/references/shared-workspace-architecture.md +680 -0
- package/.agent/skills/agent-native-architecture/references/system-prompt-design.md +250 -0
- package/.agent/skills/agent-native-reviewer/SKILL.md +246 -0
- package/.agent/skills/andrew-kane-gem-writer/SKILL.md +184 -0
- package/.agent/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/.agent/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/.agent/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/.agent/skills/andrew-kane-gem-writer/references/resources.md +119 -0
- package/.agent/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/.agent/skills/ankane-readme-writer/SKILL.md +50 -0
- package/.agent/skills/architecture-strategist/SKILL.md +52 -0
- package/.agent/skills/best-practices-researcher/SKILL.md +100 -0
- package/.agent/skills/bug-reproduction-validator/SKILL.md +67 -0
- package/.agent/skills/code-simplicity-reviewer/SKILL.md +85 -0
- package/.agent/skills/coding-tutor/.claude-plugin/plugin.json +9 -0
- package/.agent/skills/coding-tutor/README.md +37 -0
- package/.agent/skills/coding-tutor/commands/quiz-me.md +1 -0
- package/.agent/skills/coding-tutor/commands/sync-tutorials.md +25 -0
- package/.agent/skills/coding-tutor/commands/teach-me.md +1 -0
- package/.agent/skills/coding-tutor/skills/coding-tutor/SKILL.md +214 -0
- package/.agent/skills/coding-tutor/skills/coding-tutor/scripts/create_tutorial.py +202 -0
- package/.agent/skills/coding-tutor/skills/coding-tutor/scripts/index_tutorials.py +203 -0
- package/.agent/skills/coding-tutor/skills/coding-tutor/scripts/quiz_priority.py +190 -0
- package/.agent/skills/coding-tutor/skills/coding-tutor/scripts/setup_tutorials.py +132 -0
- package/.agent/skills/compound-docs/SKILL.md +510 -0
- package/.agent/skills/compound-docs/assets/critical-pattern-template.md +34 -0
- package/.agent/skills/compound-docs/assets/resolution-template.md +93 -0
- package/.agent/skills/compound-docs/references/yaml-schema.md +65 -0
- package/.agent/skills/compound-docs/schema.yaml +176 -0
- package/.agent/skills/create-agent-skills/SKILL.md +299 -0
- package/.agent/skills/create-agent-skills/references/api-security.md +226 -0
- package/.agent/skills/create-agent-skills/references/be-clear-and-direct.md +531 -0
- package/.agent/skills/create-agent-skills/references/best-practices.md +404 -0
- package/.agent/skills/create-agent-skills/references/common-patterns.md +595 -0
- package/.agent/skills/create-agent-skills/references/core-principles.md +437 -0
- package/.agent/skills/create-agent-skills/references/executable-code.md +175 -0
- package/.agent/skills/create-agent-skills/references/iteration-and-testing.md +474 -0
- package/.agent/skills/create-agent-skills/references/official-spec.md +185 -0
- package/.agent/skills/create-agent-skills/references/recommended-structure.md +168 -0
- package/.agent/skills/create-agent-skills/references/skill-structure.md +372 -0
- package/.agent/skills/create-agent-skills/references/using-scripts.md +113 -0
- package/.agent/skills/create-agent-skills/references/using-templates.md +112 -0
- package/.agent/skills/create-agent-skills/references/workflows-and-validation.md +510 -0
- package/.agent/skills/create-agent-skills/templates/router-skill.md +73 -0
- package/.agent/skills/create-agent-skills/templates/simple-skill.md +33 -0
- package/.agent/skills/create-agent-skills/workflows/add-reference.md +96 -0
- package/.agent/skills/create-agent-skills/workflows/add-script.md +93 -0
- package/.agent/skills/create-agent-skills/workflows/add-template.md +74 -0
- package/.agent/skills/create-agent-skills/workflows/add-workflow.md +120 -0
- package/.agent/skills/create-agent-skills/workflows/audit-skill.md +138 -0
- package/.agent/skills/create-agent-skills/workflows/create-domain-expertise-skill.md +605 -0
- package/.agent/skills/create-agent-skills/workflows/create-new-skill.md +191 -0
- package/.agent/skills/create-agent-skills/workflows/get-guidance.md +121 -0
- package/.agent/skills/create-agent-skills/workflows/upgrade-to-router.md +161 -0
- package/.agent/skills/create-agent-skills/workflows/verify-skill.md +204 -0
- package/.agent/skills/data-integrity-guardian/SKILL.md +70 -0
- package/.agent/skills/data-migration-expert/SKILL.md +97 -0
- package/.agent/skills/deployment-verification-agent/SKILL.md +159 -0
- package/.agent/skills/design-implementation-reviewer/SKILL.md +85 -0
- package/.agent/skills/design-iterator/SKILL.md +197 -0
- package/.agent/skills/dhh-rails-reviewer/SKILL.md +45 -0
- package/.agent/skills/dhh-rails-style/SKILL.md +184 -0
- package/.agent/skills/dhh-rails-style/references/architecture.md +653 -0
- package/.agent/skills/dhh-rails-style/references/controllers.md +303 -0
- package/.agent/skills/dhh-rails-style/references/frontend.md +510 -0
- package/.agent/skills/dhh-rails-style/references/gems.md +266 -0
- package/.agent/skills/dhh-rails-style/references/models.md +359 -0
- package/.agent/skills/dhh-rails-style/references/testing.md +338 -0
- package/.agent/skills/dspy-ruby/SKILL.md +594 -0
- package/.agent/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/.agent/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/.agent/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/.agent/skills/dspy-ruby/references/core-concepts.md +265 -0
- package/.agent/skills/dspy-ruby/references/optimization.md +623 -0
- package/.agent/skills/dspy-ruby/references/providers.md +305 -0
- package/.agent/skills/every-style-editor/SKILL.md +134 -0
- package/.agent/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/.agent/skills/figma-design-sync/SKILL.md +166 -0
- package/.agent/skills/file-todos/SKILL.md +251 -0
- package/.agent/skills/file-todos/assets/todo-template.md +155 -0
- package/.agent/skills/framework-docs-researcher/SKILL.md +83 -0
- package/.agent/skills/frontend-design/SKILL.md +42 -0
- package/.agent/skills/gemini-imagegen/SKILL.md +237 -0
- package/.agent/skills/gemini-imagegen/requirements.txt +2 -0
- package/.agent/skills/gemini-imagegen/scripts/compose_images.py +168 -0
- package/.agent/skills/gemini-imagegen/scripts/edit_image.py +157 -0
- package/.agent/skills/gemini-imagegen/scripts/gemini_images.py +265 -0
- package/.agent/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/.agent/skills/gemini-imagegen/scripts/multi_turn_chat.py +215 -0
- package/.agent/skills/git-history-analyzer/SKILL.md +42 -0
- package/.agent/skills/git-worktree/SKILL.md +302 -0
- package/.agent/skills/git-worktree/scripts/worktree-manager.sh +345 -0
- package/.agent/skills/julik-frontend-races-reviewer/SKILL.md +222 -0
- package/.agent/skills/kieran-python-reviewer/SKILL.md +104 -0
- package/.agent/skills/kieran-rails-reviewer/SKILL.md +86 -0
- package/.agent/skills/kieran-typescript-reviewer/SKILL.md +95 -0
- package/.agent/skills/lint/SKILL.md +16 -0
- package/.agent/skills/pattern-recognition-specialist/SKILL.md +57 -0
- package/.agent/skills/performance-oracle/SKILL.md +110 -0
- package/.agent/skills/pr-comment-resolver/SKILL.md +69 -0
- package/.agent/skills/rclone/SKILL.md +150 -0
- package/.agent/skills/rclone/scripts/check_setup.sh +60 -0
- package/.agent/skills/repo-research-analyst/SKILL.md +113 -0
- package/.agent/skills/security-sentinel/SKILL.md +93 -0
- package/.agent/skills/skill-creator/SKILL.md +209 -0
- package/.agent/skills/skill-creator/scripts/init_skill.py +304 -0
- package/.agent/skills/skill-creator/scripts/package_skill.py +112 -0
- package/.agent/skills/skill-creator/scripts/quick_validate.py +72 -0
- package/.agent/skills/spec-flow-analyzer/SKILL.md +113 -0
- package/.agent/skills/test-agent/SKILL.md +4 -0
- package/.agent/workflows/agent-native-audit.md +277 -0
- package/.agent/workflows/ask-user-question.md +21 -0
- package/.agent/workflows/changelog.md +137 -0
- package/.agent/workflows/compound.md +202 -0
- package/.agent/workflows/create-agent-skill.md +8 -0
- package/.agent/workflows/deepen-plan-research.md +334 -0
- package/.agent/workflows/deepen-plan-synthesis.md +182 -0
- package/.agent/workflows/deepen-plan.md +79 -0
- package/.agent/workflows/feature-video.md +342 -0
- package/.agent/workflows/generate-command.md +162 -0
- package/.agent/workflows/heal-skill.md +142 -0
- package/.agent/workflows/lfg.md +20 -0
- package/.agent/workflows/plan-analysis.md +67 -0
- package/.agent/workflows/plan-next-steps.md +63 -0
- package/.agent/workflows/plan-review.md +33 -0
- package/.agent/workflows/plan-synthesis.md +106 -0
- package/.agent/workflows/plan.md +49 -0
- package/.agent/workflows/report-bug.md +150 -0
- package/.agent/workflows/reproduce-bug.md +99 -0
- package/.agent/workflows/resolve-parallel.md +34 -0
- package/.agent/workflows/resolve-pr-parallel.md +49 -0
- package/.agent/workflows/resolve-todo-parallel.md +35 -0
- package/.agent/workflows/review-analysis.md +145 -0
- package/.agent/workflows/review-synthesis.md +262 -0
- package/.agent/workflows/review.md +64 -0
- package/.agent/workflows/ship.md +90 -0
- package/.agent/workflows/test-command.md +3 -0
- package/.agent/workflows/triage.md +310 -0
- package/.agent/workflows/work.md +157 -0
- package/.agent/workflows/xcode-test.md +332 -0
- package/LICENSE +22 -0
- package/README.md +49 -0
- package/bin/ag-cortex.js +54 -0
- package/lib/core.js +165 -0
- package/package.json +31 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# Architecture - DHH Rails Style
|
|
2
|
+
|
|
3
|
+
<routing>
|
|
4
|
+
## Routing
|
|
5
|
+
|
|
6
|
+
Everything maps to CRUD. Nested resources for related actions:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
Rails.application.routes.draw do
|
|
10
|
+
resources :boards do
|
|
11
|
+
resources :cards do
|
|
12
|
+
resource :closure
|
|
13
|
+
resource :goldness
|
|
14
|
+
resource :not_now
|
|
15
|
+
resources :assignments
|
|
16
|
+
resources :comments
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Verb-to-noun conversion:**
|
|
23
|
+
| Action | Resource |
|
|
24
|
+
|--------|----------|
|
|
25
|
+
| close a card | `card.closure` |
|
|
26
|
+
| watch a board | `board.watching` |
|
|
27
|
+
| mark as golden | `card.goldness` |
|
|
28
|
+
| archive a card | `card.archival` |
|
|
29
|
+
|
|
30
|
+
**Shallow nesting** - avoid deep URLs:
|
|
31
|
+
```ruby
|
|
32
|
+
resources :boards do
|
|
33
|
+
resources :cards, shallow: true # /boards/:id/cards, but /cards/:id
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Singular resources** for one-per-parent:
|
|
38
|
+
```ruby
|
|
39
|
+
resource :closure # not resources
|
|
40
|
+
resource :goldness
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Resolve for URL generation:**
|
|
44
|
+
```ruby
|
|
45
|
+
# config/routes.rb
|
|
46
|
+
resolve("Comment") { |comment| [comment.card, anchor: dom_id(comment)] }
|
|
47
|
+
|
|
48
|
+
# Now url_for(@comment) works correctly
|
|
49
|
+
```
|
|
50
|
+
</routing>
|
|
51
|
+
|
|
52
|
+
<multi_tenancy>
|
|
53
|
+
## Multi-Tenancy (Path-Based)
|
|
54
|
+
|
|
55
|
+
**Middleware extracts tenant** from URL prefix:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# lib/tenant_extractor.rb
|
|
59
|
+
class TenantExtractor
|
|
60
|
+
def initialize(app)
|
|
61
|
+
@app = app
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(env)
|
|
65
|
+
path = env["PATH_INFO"]
|
|
66
|
+
if match = path.match(%r{^/(\d+)(/.*)?$})
|
|
67
|
+
env["SCRIPT_NAME"] = "/#{match[1]}"
|
|
68
|
+
env["PATH_INFO"] = match[2] || "/"
|
|
69
|
+
end
|
|
70
|
+
@app.call(env)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Cookie scoping** per tenant:
|
|
76
|
+
```ruby
|
|
77
|
+
# Cookies scoped to tenant path
|
|
78
|
+
cookies.signed[:session_id] = {
|
|
79
|
+
value: session.id,
|
|
80
|
+
path: "/#{Current.account.id}"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Background job context** - serialize tenant:
|
|
85
|
+
```ruby
|
|
86
|
+
class ApplicationJob < ActiveJob::Base
|
|
87
|
+
around_perform do |job, block|
|
|
88
|
+
Current.set(account: job.arguments.first.account) { block.call }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Recurring jobs** must iterate all tenants:
|
|
94
|
+
```ruby
|
|
95
|
+
class DailyDigestJob < ApplicationJob
|
|
96
|
+
def perform
|
|
97
|
+
Account.find_each do |account|
|
|
98
|
+
Current.set(account: account) do
|
|
99
|
+
send_digest_for(account)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Controller security** - always scope through tenant:
|
|
107
|
+
```ruby
|
|
108
|
+
# Good - scoped through user's accessible records
|
|
109
|
+
@card = Current.user.accessible_cards.find(params[:id])
|
|
110
|
+
|
|
111
|
+
# Avoid - direct lookup
|
|
112
|
+
@card = Card.find(params[:id])
|
|
113
|
+
```
|
|
114
|
+
</multi_tenancy>
|
|
115
|
+
|
|
116
|
+
<authentication>
|
|
117
|
+
## Authentication
|
|
118
|
+
|
|
119
|
+
Custom passwordless magic link auth (~150 lines total):
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# app/models/session.rb
|
|
123
|
+
class Session < ApplicationRecord
|
|
124
|
+
belongs_to :user
|
|
125
|
+
|
|
126
|
+
before_create { self.token = SecureRandom.urlsafe_base64(32) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# app/models/magic_link.rb
|
|
130
|
+
class MagicLink < ApplicationRecord
|
|
131
|
+
belongs_to :user
|
|
132
|
+
|
|
133
|
+
before_create do
|
|
134
|
+
self.code = SecureRandom.random_number(100_000..999_999).to_s
|
|
135
|
+
self.expires_at = 15.minutes.from_now
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def expired?
|
|
139
|
+
expires_at < Time.current
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Why not Devise:**
|
|
145
|
+
- ~150 lines vs massive dependency
|
|
146
|
+
- No password storage liability
|
|
147
|
+
- Simpler UX for users
|
|
148
|
+
- Full control over flow
|
|
149
|
+
|
|
150
|
+
**Bearer token** for APIs:
|
|
151
|
+
```ruby
|
|
152
|
+
module Authentication
|
|
153
|
+
extend ActiveSupport::Concern
|
|
154
|
+
|
|
155
|
+
included do
|
|
156
|
+
before_action :authenticate
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
def authenticate
|
|
161
|
+
if bearer_token = request.headers["Authorization"]&.split(" ")&.last
|
|
162
|
+
Current.session = Session.find_by(token: bearer_token)
|
|
163
|
+
else
|
|
164
|
+
Current.session = Session.find_by(id: cookies.signed[:session_id])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
redirect_to login_path unless Current.session
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
</authentication>
|
|
172
|
+
|
|
173
|
+
<background_jobs>
|
|
174
|
+
## Background Jobs
|
|
175
|
+
|
|
176
|
+
Jobs are shallow wrappers calling model methods:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class NotifyWatchersJob < ApplicationJob
|
|
180
|
+
def perform(card)
|
|
181
|
+
card.notify_watchers
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Naming convention:**
|
|
187
|
+
- `_later` suffix for async: `card.notify_watchers_later`
|
|
188
|
+
- `_now` suffix for immediate: `card.notify_watchers_now`
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
module Watchable
|
|
192
|
+
def notify_watchers_later
|
|
193
|
+
NotifyWatchersJob.perform_later(self)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def notify_watchers_now
|
|
197
|
+
NotifyWatchersJob.perform_now(self)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def notify_watchers
|
|
201
|
+
watchers.each do |watcher|
|
|
202
|
+
WatcherMailer.notification(watcher, self).deliver_later
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Database-backed** with Solid Queue:
|
|
209
|
+
- No Redis required
|
|
210
|
+
- Same transactional guarantees as your data
|
|
211
|
+
- Simpler infrastructure
|
|
212
|
+
|
|
213
|
+
**Transaction safety:**
|
|
214
|
+
```ruby
|
|
215
|
+
# config/application.rb
|
|
216
|
+
config.active_job.enqueue_after_transaction_commit = true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Error handling** by type:
|
|
220
|
+
```ruby
|
|
221
|
+
class DeliveryJob < ApplicationJob
|
|
222
|
+
# Transient errors - retry with backoff
|
|
223
|
+
retry_on Net::OpenTimeout, Net::ReadTimeout,
|
|
224
|
+
Resolv::ResolvError,
|
|
225
|
+
wait: :polynomially_longer
|
|
226
|
+
|
|
227
|
+
# Permanent errors - log and discard
|
|
228
|
+
discard_on Net::SMTPSyntaxError do |job, error|
|
|
229
|
+
Sentry.capture_exception(error, level: :info)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Batch processing** with continuable:
|
|
235
|
+
```ruby
|
|
236
|
+
class ProcessCardsJob < ApplicationJob
|
|
237
|
+
include ActiveJob::Continuable
|
|
238
|
+
|
|
239
|
+
def perform
|
|
240
|
+
Card.in_batches.each_record do |card|
|
|
241
|
+
checkpoint! # Resume from here if interrupted
|
|
242
|
+
process(card)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
</background_jobs>
|
|
248
|
+
|
|
249
|
+
<database_patterns>
|
|
250
|
+
## Database Patterns
|
|
251
|
+
|
|
252
|
+
**UUIDs as primary keys** (time-sortable UUIDv7):
|
|
253
|
+
```ruby
|
|
254
|
+
# migration
|
|
255
|
+
create_table :cards, id: :uuid do |t|
|
|
256
|
+
t.references :board, type: :uuid, foreign_key: true
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Benefits: No ID enumeration, distributed-friendly, client-side generation.
|
|
261
|
+
|
|
262
|
+
**State as records** (not booleans):
|
|
263
|
+
```ruby
|
|
264
|
+
# Instead of closed: boolean
|
|
265
|
+
class Card::Closure < ApplicationRecord
|
|
266
|
+
belongs_to :card
|
|
267
|
+
belongs_to :creator, class_name: "User"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Queries become joins
|
|
271
|
+
Card.joins(:closure) # closed
|
|
272
|
+
Card.where.missing(:closure) # open
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Hard deletes** - no soft delete:
|
|
276
|
+
```ruby
|
|
277
|
+
# Just destroy
|
|
278
|
+
card.destroy!
|
|
279
|
+
|
|
280
|
+
# Use events for history
|
|
281
|
+
card.record_event(:deleted, by: Current.user)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Simplifies queries, uses event logs for auditing.
|
|
285
|
+
|
|
286
|
+
**Counter caches** for performance:
|
|
287
|
+
```ruby
|
|
288
|
+
class Comment < ApplicationRecord
|
|
289
|
+
belongs_to :card, counter_cache: true
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# card.comments_count available without query
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Account scoping** on every table:
|
|
296
|
+
```ruby
|
|
297
|
+
class Card < ApplicationRecord
|
|
298
|
+
belongs_to :account
|
|
299
|
+
default_scope { where(account: Current.account) }
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
</database_patterns>
|
|
303
|
+
|
|
304
|
+
<current_attributes>
|
|
305
|
+
## Current Attributes
|
|
306
|
+
|
|
307
|
+
Use `Current` for request-scoped state:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# app/models/current.rb
|
|
311
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
312
|
+
attribute :session, :user, :account, :request_id
|
|
313
|
+
|
|
314
|
+
delegate :user, to: :session, allow_nil: true
|
|
315
|
+
|
|
316
|
+
def account=(account)
|
|
317
|
+
super
|
|
318
|
+
Time.zone = account&.time_zone || "UTC"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Set in controller:
|
|
324
|
+
```ruby
|
|
325
|
+
class ApplicationController < ActionController::Base
|
|
326
|
+
before_action :set_current_request
|
|
327
|
+
|
|
328
|
+
private
|
|
329
|
+
def set_current_request
|
|
330
|
+
Current.session = authenticated_session
|
|
331
|
+
Current.account = Account.find(params[:account_id])
|
|
332
|
+
Current.request_id = request.request_id
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Use throughout app:
|
|
338
|
+
```ruby
|
|
339
|
+
class Card < ApplicationRecord
|
|
340
|
+
belongs_to :creator, default: -> { Current.user }
|
|
341
|
+
end
|
|
342
|
+
```
|
|
343
|
+
</current_attributes>
|
|
344
|
+
|
|
345
|
+
<caching>
|
|
346
|
+
## Caching
|
|
347
|
+
|
|
348
|
+
**HTTP caching** with ETags:
|
|
349
|
+
```ruby
|
|
350
|
+
fresh_when etag: [@card, Current.user.timezone]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Fragment caching:**
|
|
354
|
+
```erb
|
|
355
|
+
<% cache card do %>
|
|
356
|
+
<%= render card %>
|
|
357
|
+
<% end %>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Russian doll caching:**
|
|
361
|
+
```erb
|
|
362
|
+
<% cache @board do %>
|
|
363
|
+
<% @board.cards.each do |card| %>
|
|
364
|
+
<% cache card do %>
|
|
365
|
+
<%= render card %>
|
|
366
|
+
<% end %>
|
|
367
|
+
<% end %>
|
|
368
|
+
<% end %>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Cache invalidation** via `touch: true`:
|
|
372
|
+
```ruby
|
|
373
|
+
class Card < ApplicationRecord
|
|
374
|
+
belongs_to :board, touch: true
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Solid Cache** - database-backed:
|
|
379
|
+
- No Redis required
|
|
380
|
+
- Consistent with application data
|
|
381
|
+
- Simpler infrastructure
|
|
382
|
+
</caching>
|
|
383
|
+
|
|
384
|
+
<configuration>
|
|
385
|
+
## Configuration
|
|
386
|
+
|
|
387
|
+
**ENV.fetch with defaults:**
|
|
388
|
+
```ruby
|
|
389
|
+
# config/application.rb
|
|
390
|
+
config.active_job.queue_adapter = ENV.fetch("QUEUE_ADAPTER", "solid_queue").to_sym
|
|
391
|
+
config.cache_store = ENV.fetch("CACHE_STORE", "solid_cache").to_sym
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Multiple databases:**
|
|
395
|
+
```yaml
|
|
396
|
+
# config/database.yml
|
|
397
|
+
production:
|
|
398
|
+
primary:
|
|
399
|
+
<<: *default
|
|
400
|
+
cable:
|
|
401
|
+
<<: *default
|
|
402
|
+
migrations_paths: db/cable_migrate
|
|
403
|
+
queue:
|
|
404
|
+
<<: *default
|
|
405
|
+
migrations_paths: db/queue_migrate
|
|
406
|
+
cache:
|
|
407
|
+
<<: *default
|
|
408
|
+
migrations_paths: db/cache_migrate
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Switch between SQLite and MySQL via ENV:**
|
|
412
|
+
```ruby
|
|
413
|
+
adapter = ENV.fetch("DATABASE_ADAPTER", "sqlite3")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**CSP extensible via ENV:**
|
|
417
|
+
```ruby
|
|
418
|
+
config.content_security_policy do |policy|
|
|
419
|
+
policy.default_src :self
|
|
420
|
+
policy.script_src :self, *ENV.fetch("CSP_SCRIPT_SRC", "").split(",")
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
</configuration>
|
|
424
|
+
|
|
425
|
+
<testing>
|
|
426
|
+
## Testing
|
|
427
|
+
|
|
428
|
+
**Minitest**, not RSpec:
|
|
429
|
+
```ruby
|
|
430
|
+
class CardTest < ActiveSupport::TestCase
|
|
431
|
+
test "closing a card creates a closure" do
|
|
432
|
+
card = cards(:one)
|
|
433
|
+
|
|
434
|
+
card.close
|
|
435
|
+
|
|
436
|
+
assert card.closed?
|
|
437
|
+
assert_not_nil card.closure
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Fixtures** instead of factories:
|
|
443
|
+
```yaml
|
|
444
|
+
# test/fixtures/cards.yml
|
|
445
|
+
one:
|
|
446
|
+
title: First Card
|
|
447
|
+
board: main
|
|
448
|
+
creator: alice
|
|
449
|
+
|
|
450
|
+
two:
|
|
451
|
+
title: Second Card
|
|
452
|
+
board: main
|
|
453
|
+
creator: bob
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Integration tests** for controllers:
|
|
457
|
+
```ruby
|
|
458
|
+
class CardsControllerTest < ActionDispatch::IntegrationTest
|
|
459
|
+
test "closing a card" do
|
|
460
|
+
card = cards(:one)
|
|
461
|
+
sign_in users(:alice)
|
|
462
|
+
|
|
463
|
+
post card_closure_path(card)
|
|
464
|
+
|
|
465
|
+
assert_response :success
|
|
466
|
+
assert card.reload.closed?
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Tests ship with features** - same commit, not TDD-first but together.
|
|
472
|
+
|
|
473
|
+
**Regression tests for security fixes** - always.
|
|
474
|
+
</testing>
|
|
475
|
+
|
|
476
|
+
<events>
|
|
477
|
+
## Event Tracking
|
|
478
|
+
|
|
479
|
+
Events are the single source of truth:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
class Event < ApplicationRecord
|
|
483
|
+
belongs_to :creator, class_name: "User"
|
|
484
|
+
belongs_to :eventable, polymorphic: true
|
|
485
|
+
|
|
486
|
+
serialize :particulars, coder: JSON
|
|
487
|
+
end
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Eventable concern:**
|
|
491
|
+
```ruby
|
|
492
|
+
module Eventable
|
|
493
|
+
extend ActiveSupport::Concern
|
|
494
|
+
|
|
495
|
+
included do
|
|
496
|
+
has_many :events, as: :eventable, dependent: :destroy
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def record_event(action, particulars = {})
|
|
500
|
+
events.create!(
|
|
501
|
+
creator: Current.user,
|
|
502
|
+
action: action,
|
|
503
|
+
particulars: particulars
|
|
504
|
+
)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Webhooks driven by events** - events are the canonical source.
|
|
510
|
+
</events>
|
|
511
|
+
|
|
512
|
+
<email_patterns>
|
|
513
|
+
## Email Patterns
|
|
514
|
+
|
|
515
|
+
**Multi-tenant URL helpers:**
|
|
516
|
+
```ruby
|
|
517
|
+
class ApplicationMailer < ActionMailer::Base
|
|
518
|
+
def default_url_options
|
|
519
|
+
options = super
|
|
520
|
+
if Current.account
|
|
521
|
+
options[:script_name] = "/#{Current.account.id}"
|
|
522
|
+
end
|
|
523
|
+
options
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Timezone-aware delivery:**
|
|
529
|
+
```ruby
|
|
530
|
+
class NotificationMailer < ApplicationMailer
|
|
531
|
+
def daily_digest(user)
|
|
532
|
+
Time.use_zone(user.timezone) do
|
|
533
|
+
@user = user
|
|
534
|
+
@digest = user.digest_for_today
|
|
535
|
+
mail(to: user.email, subject: "Daily Digest")
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Batch delivery:**
|
|
542
|
+
```ruby
|
|
543
|
+
emails = users.map { |user| NotificationMailer.digest(user) }
|
|
544
|
+
ActiveJob.perform_all_later(emails.map(&:deliver_later))
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**One-click unsubscribe (RFC 8058):**
|
|
548
|
+
```ruby
|
|
549
|
+
class ApplicationMailer < ActionMailer::Base
|
|
550
|
+
after_action :set_unsubscribe_headers
|
|
551
|
+
|
|
552
|
+
private
|
|
553
|
+
def set_unsubscribe_headers
|
|
554
|
+
headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
|
|
555
|
+
headers["List-Unsubscribe"] = "<#{unsubscribe_url}>"
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
</email_patterns>
|
|
560
|
+
|
|
561
|
+
<security_patterns>
|
|
562
|
+
## Security Patterns
|
|
563
|
+
|
|
564
|
+
**XSS prevention** - escape in helpers:
|
|
565
|
+
```ruby
|
|
566
|
+
def formatted_content(text)
|
|
567
|
+
# Escape first, then mark safe
|
|
568
|
+
simple_format(h(text)).html_safe
|
|
569
|
+
end
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**SSRF protection:**
|
|
573
|
+
```ruby
|
|
574
|
+
# Resolve DNS once, pin the IP
|
|
575
|
+
def fetch_safely(url)
|
|
576
|
+
uri = URI.parse(url)
|
|
577
|
+
ip = Resolv.getaddress(uri.host)
|
|
578
|
+
|
|
579
|
+
# Block private networks
|
|
580
|
+
raise "Private IP" if private_ip?(ip)
|
|
581
|
+
|
|
582
|
+
# Use pinned IP for request
|
|
583
|
+
Net::HTTP.start(uri.host, uri.port, ipaddr: ip) { |http| ... }
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def private_ip?(ip)
|
|
587
|
+
ip.start_with?("127.", "10.", "192.168.") ||
|
|
588
|
+
ip.match?(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
|
|
589
|
+
end
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Content Security Policy:**
|
|
593
|
+
```ruby
|
|
594
|
+
# config/initializers/content_security_policy.rb
|
|
595
|
+
Rails.application.configure do
|
|
596
|
+
config.content_security_policy do |policy|
|
|
597
|
+
policy.default_src :self
|
|
598
|
+
policy.script_src :self
|
|
599
|
+
policy.style_src :self, :unsafe_inline
|
|
600
|
+
policy.base_uri :none
|
|
601
|
+
policy.form_action :self
|
|
602
|
+
policy.frame_ancestors :self
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**ActionText sanitization:**
|
|
608
|
+
```ruby
|
|
609
|
+
# config/initializers/action_text.rb
|
|
610
|
+
Rails.application.config.after_initialize do
|
|
611
|
+
ActionText::ContentHelper.allowed_tags = %w[
|
|
612
|
+
strong em a ul ol li p br h1 h2 h3 h4 blockquote
|
|
613
|
+
]
|
|
614
|
+
end
|
|
615
|
+
```
|
|
616
|
+
</security_patterns>
|
|
617
|
+
|
|
618
|
+
<active_storage>
|
|
619
|
+
## Active Storage Patterns
|
|
620
|
+
|
|
621
|
+
**Variant preprocessing:**
|
|
622
|
+
```ruby
|
|
623
|
+
class User < ApplicationRecord
|
|
624
|
+
has_one_attached :avatar do |attachable|
|
|
625
|
+
attachable.variant :thumb, resize_to_limit: [100, 100], preprocessed: true
|
|
626
|
+
attachable.variant :medium, resize_to_limit: [300, 300], preprocessed: true
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Direct upload expiry** - extend for slow connections:
|
|
632
|
+
```ruby
|
|
633
|
+
# config/initializers/active_storage.rb
|
|
634
|
+
Rails.application.config.active_storage.service_urls_expire_in = 48.hours
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Avatar optimization** - redirect to blob:
|
|
638
|
+
```ruby
|
|
639
|
+
def show
|
|
640
|
+
expires_in 1.year, public: true
|
|
641
|
+
redirect_to @user.avatar.variant(:thumb).processed.url, allow_other_host: true
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Mirror service** for migrations:
|
|
646
|
+
```yaml
|
|
647
|
+
# config/storage.yml
|
|
648
|
+
production:
|
|
649
|
+
service: Mirror
|
|
650
|
+
primary: amazon
|
|
651
|
+
mirrors: [google]
|
|
652
|
+
```
|
|
653
|
+
</active_storage>
|