red64-cli 0.1.0 → 0.3.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/README.md +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# AI/LLM Implementation with ruby_llm
|
|
2
|
+
|
|
3
|
+
Project memory for implementing AI-powered features using the ruby_llm gem in MediaPulse.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
MediaPulse uses the `ruby_llm` gem as a unified interface for multi-provider LLM access. The architecture separates concerns into configuration, client factory, and domain-specific services.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Architecture Layers
|
|
14
|
+
|
|
15
|
+
### 1. Configuration (`config/initializers/ruby_llm.rb`)
|
|
16
|
+
Global setup for ruby_llm gem with API keys and defaults.
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Pattern: Configure in initializer, credentials from Rails.credentials
|
|
20
|
+
RubyLLM.configure do |config|
|
|
21
|
+
config.anthropic_api_key = Rails.application.credentials.dig(:anthropic, :api_key)
|
|
22
|
+
config.openai_api_key = Rails.application.credentials.dig(:openai, :api_key)
|
|
23
|
+
config.default_model = "claude-sonnet-4-20250514"
|
|
24
|
+
config.max_retries = 3
|
|
25
|
+
config.retry_interval = 0.5
|
|
26
|
+
config.retry_backoff_factor = 2
|
|
27
|
+
config.request_timeout = 120
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Client Factory (`app/services/llm_client_factory.rb`)
|
|
32
|
+
Provider abstraction with task-specific model selection.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Pattern: Factory methods for different use cases
|
|
36
|
+
LlmClientFactory.extraction_client # Claude Haiku - fast, cost-effective
|
|
37
|
+
LlmClientFactory.generation_client # Claude Sonnet - creative, quality
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Service Layer (`app/services/llm_service.rb`)
|
|
41
|
+
Unified interface for AI operations with error mapping.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# Pattern: Domain methods with consistent return types
|
|
45
|
+
LlmService.generate_ideas(prompt:, system_prompt:, model:)
|
|
46
|
+
LlmService.generate_content(prompt:, system_prompt:, model:)
|
|
47
|
+
LlmService.stream(prompt:, system_prompt:, model:, &block)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Model Selection Strategy
|
|
53
|
+
|
|
54
|
+
| Use Case | Model | Rationale |
|
|
55
|
+
|----------|-------|-----------|
|
|
56
|
+
| **Extraction** (semantic, entities) | Claude Haiku | Fast, cost-effective for structured extraction |
|
|
57
|
+
| **Generation** (ideas, content) | Claude Sonnet | Higher quality for creative tasks |
|
|
58
|
+
| **Embeddings** | OpenAI text-embedding-3-small | Cost-effective vector generation |
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# Constants for model IDs (avoid magic strings)
|
|
62
|
+
HAIKU_MODEL = "claude-haiku-3-5-20241022"
|
|
63
|
+
SONNET_MODEL = "claude-sonnet-4-20250514"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Response Handling Pattern
|
|
69
|
+
|
|
70
|
+
Use immutable Data objects for LLM responses:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Pattern: Immutable value objects for responses
|
|
74
|
+
LlmResponse = Data.define(:content, :model, :input_tokens, :output_tokens)
|
|
75
|
+
|
|
76
|
+
# Usage in service
|
|
77
|
+
def build_response(response)
|
|
78
|
+
LlmResponse.new(
|
|
79
|
+
content: response.content,
|
|
80
|
+
model: response.model_id,
|
|
81
|
+
input_tokens: response.input_tokens,
|
|
82
|
+
output_tokens: response.output_tokens
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Prompt Engineering Patterns
|
|
90
|
+
|
|
91
|
+
### System Prompts
|
|
92
|
+
Define persona and output format constraints:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# Pattern: Constants for reusable system prompts
|
|
96
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
97
|
+
You are an expert content strategist. Analyze the provided source material and generate
|
|
98
|
+
compelling content ideas. For each idea, provide:
|
|
99
|
+
1. A clear, engaging title
|
|
100
|
+
2. A brief summary (2-3 sentences)
|
|
101
|
+
3. The source indices that support this idea
|
|
102
|
+
|
|
103
|
+
Return your response as JSON array with objects containing: title, summary, source_indices
|
|
104
|
+
PROMPT
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### JSON Output Requests
|
|
108
|
+
Request structured output with explicit format:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Pattern: Explicit JSON schema in prompt
|
|
112
|
+
def build_prompt(content, source_type)
|
|
113
|
+
<<~PROMPT
|
|
114
|
+
Analyze the following #{source_type} content and extract semantic information.
|
|
115
|
+
|
|
116
|
+
Return a JSON object with exactly these fields:
|
|
117
|
+
- summary: A 2-3 sentence summary
|
|
118
|
+
- entities: Array of objects with "name" and "type" keys
|
|
119
|
+
- claims: Array of strings representing key assertions
|
|
120
|
+
- confidence: A number between 0.0 and 1.0
|
|
121
|
+
|
|
122
|
+
Respond with ONLY valid JSON, no markdown formatting or explanation.
|
|
123
|
+
|
|
124
|
+
CONTENT:
|
|
125
|
+
#{content}
|
|
126
|
+
PROMPT
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Content Truncation
|
|
131
|
+
Handle token limits gracefully:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# Pattern: Truncate before sending to LLM
|
|
135
|
+
MAX_CONTENT_TOKENS = 8000
|
|
136
|
+
CHARS_PER_TOKEN = 4
|
|
137
|
+
|
|
138
|
+
def truncate_content(content)
|
|
139
|
+
max_chars = MAX_CONTENT_TOKENS * CHARS_PER_TOKEN
|
|
140
|
+
return content if content.length <= max_chars
|
|
141
|
+
content[0, max_chars] + "\n\n[Content truncated for processing]"
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## JSON Response Parsing
|
|
148
|
+
|
|
149
|
+
LLMs may wrap JSON in markdown code fences:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Pattern: Extract JSON from potential markdown wrapping
|
|
153
|
+
def extract_json(content)
|
|
154
|
+
content = content.strip
|
|
155
|
+
if content.start_with?("```")
|
|
156
|
+
content = content.sub(/\A```\w*\n?/, "")
|
|
157
|
+
content = content.sub(/\n?```\z/, "")
|
|
158
|
+
end
|
|
159
|
+
content.strip
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Pattern: Graceful fallback for parsing failures
|
|
163
|
+
def parse_response(response_content)
|
|
164
|
+
json_content = extract_json(response_content)
|
|
165
|
+
parsed = JSON.parse(json_content)
|
|
166
|
+
return {} unless parsed.is_a?(Hash)
|
|
167
|
+
parsed.transform_keys(&:to_sym)
|
|
168
|
+
rescue JSON::ParserError => e
|
|
169
|
+
Rails.logger.warn("Failed to parse JSON response: #{e.message}")
|
|
170
|
+
{}
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
### Error Hierarchy
|
|
179
|
+
Map ruby_llm errors to domain-specific errors:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Pattern: Custom error hierarchy
|
|
183
|
+
class LlmService
|
|
184
|
+
class Error < StandardError; end
|
|
185
|
+
class ConfigurationError < Error; end
|
|
186
|
+
class AuthenticationError < Error; end
|
|
187
|
+
class RateLimitError < Error; end
|
|
188
|
+
class ApiError < Error; end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Pattern: Map provider errors to domain errors
|
|
192
|
+
rescue RubyLLM::UnauthorizedError => e
|
|
193
|
+
raise AuthenticationError, e.message
|
|
194
|
+
rescue RubyLLM::RateLimitError => e
|
|
195
|
+
raise RateLimitError, e.message
|
|
196
|
+
rescue RubyLLM::ConfigurationError => e
|
|
197
|
+
raise ConfigurationError, e.message
|
|
198
|
+
rescue RubyLLM::Error => e
|
|
199
|
+
raise ApiError, e.message
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Validation Before API Calls
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# Pattern: Fail fast with clear messages
|
|
206
|
+
def validate_api_key!
|
|
207
|
+
api_key = Rails.application.credentials.dig(:anthropic, :api_key)
|
|
208
|
+
return if api_key.present?
|
|
209
|
+
|
|
210
|
+
raise ConfigurationError,
|
|
211
|
+
"Anthropic API key not configured. Add to credentials: anthropic.api_key"
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Testing LLM Services
|
|
218
|
+
|
|
219
|
+
### Mocking ruby_llm Calls
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# Pattern: Stub RubyLLM.chat for unit tests
|
|
223
|
+
test "generate_ideas returns LlmResponse" do
|
|
224
|
+
mock_response = Minitest::Mock.new
|
|
225
|
+
mock_response.expect :content, "Generated content"
|
|
226
|
+
mock_response.expect :model_id, "claude-sonnet-4-20250514"
|
|
227
|
+
mock_response.expect :input_tokens, 150
|
|
228
|
+
mock_response.expect :output_tokens, 200
|
|
229
|
+
|
|
230
|
+
mock_chat = Minitest::Mock.new
|
|
231
|
+
mock_chat.expect :with_instructions, mock_chat, [String]
|
|
232
|
+
mock_chat.expect :ask, mock_response, [String]
|
|
233
|
+
|
|
234
|
+
RubyLLM.stub :chat, mock_chat do
|
|
235
|
+
result = LlmService.generate_ideas(prompt: "Test", system_prompt: "Expert")
|
|
236
|
+
assert_instance_of LlmResponse, result
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Stubbing Credentials in Tests
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# Pattern: Stub credentials for test isolation
|
|
245
|
+
def stub_anthropic_api_key
|
|
246
|
+
credentials_mock = Class.new do
|
|
247
|
+
def dig(*keys)
|
|
248
|
+
keys == [:anthropic, :api_key] ? "test-api-key" : nil
|
|
249
|
+
end
|
|
250
|
+
end.new
|
|
251
|
+
Rails.application.instance_variable_set(:@credentials, credentials_mock)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def reset_credentials_stub
|
|
255
|
+
Rails.application.instance_variable_set(:@credentials, nil)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Testing Error Mapping
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# Pattern: Verify error mapping with mock errors
|
|
263
|
+
test "maps RubyLLM::RateLimitError to RateLimitError" do
|
|
264
|
+
error_response = OpenStruct.new(body: "Rate limit exceeded")
|
|
265
|
+
mock_chat = Minitest::Mock.new
|
|
266
|
+
mock_chat.expect :ask, nil do
|
|
267
|
+
raise RubyLLM::RateLimitError.new(error_response)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
RubyLLM.stub :chat, mock_chat do
|
|
271
|
+
assert_raises(LlmService::RateLimitError) do
|
|
272
|
+
LlmService.generate_ideas(prompt: "Test")
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Credential Management
|
|
281
|
+
|
|
282
|
+
Store API keys in Rails encrypted credentials:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Edit credentials
|
|
286
|
+
bin/rails credentials:edit
|
|
287
|
+
|
|
288
|
+
# Structure
|
|
289
|
+
anthropic:
|
|
290
|
+
api_key: sk-ant-...
|
|
291
|
+
openai:
|
|
292
|
+
api_key: sk-...
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Access pattern:
|
|
296
|
+
```ruby
|
|
297
|
+
Rails.application.credentials.dig(:anthropic, :api_key)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Domain-Specific Services
|
|
303
|
+
|
|
304
|
+
Create focused services for specific AI tasks:
|
|
305
|
+
|
|
306
|
+
| Service | Purpose | Model |
|
|
307
|
+
|---------|---------|-------|
|
|
308
|
+
| `SemanticProcessing::SemanticExtractor` | Extract entities, claims, topics | Haiku |
|
|
309
|
+
| `IdeaGenerationService` | Generate content ideas from sources | Sonnet |
|
|
310
|
+
| `ContentGenerationService` | Create platform-specific content | Sonnet |
|
|
311
|
+
| `EmbeddingService` | Vector embeddings for similarity | OpenAI |
|
|
312
|
+
|
|
313
|
+
Each follows the pattern:
|
|
314
|
+
- Class method `call` for invocation
|
|
315
|
+
- Dependency injection for testability
|
|
316
|
+
- Result objects for outcomes
|
|
317
|
+
- Error propagation to caller
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Quick Reference
|
|
322
|
+
|
|
323
|
+
### Creating a New LLM Service
|
|
324
|
+
|
|
325
|
+
1. Define class in `app/services/`
|
|
326
|
+
2. Use `LlmClientFactory` for client (extraction or generation)
|
|
327
|
+
3. Build prompt with explicit JSON schema
|
|
328
|
+
4. Parse response with JSON extraction helper
|
|
329
|
+
5. Return immutable result object
|
|
330
|
+
6. Map errors to domain-specific types
|
|
331
|
+
|
|
332
|
+
### Adding Tests
|
|
333
|
+
|
|
334
|
+
1. Stub `RubyLLM.chat` with mock
|
|
335
|
+
2. Stub credentials if validating API keys
|
|
336
|
+
3. Test success path with mock responses
|
|
337
|
+
4. Test error paths with raised exceptions
|
|
338
|
+
5. Verify response structure and types
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
_Focus on patterns, not exhaustive API documentation. See ruby_llm gem docs for full API reference._
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Ruby on Rails Conventions
|
|
2
|
+
|
|
3
|
+
Project memory for Rails 8.1 patterns and conventions in MediaPulse.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Framework Stack
|
|
8
|
+
|
|
9
|
+
### Core Technologies
|
|
10
|
+
- **Rails 8.1** with `config.load_defaults 8.1`
|
|
11
|
+
- **Hotwire**: Turbo + Stimulus for reactive UI without heavy JavaScript
|
|
12
|
+
- **Propshaft**: Modern asset pipeline (not Sprockets)
|
|
13
|
+
- **Importmap**: ESM-based JavaScript, no bundler required
|
|
14
|
+
- **Solid Queue**: Database-backed job processing (replaces Sidekiq/Redis)
|
|
15
|
+
- **Solid Cache**: Database-backed caching
|
|
16
|
+
- **Kamal**: Docker-based deployment
|
|
17
|
+
|
|
18
|
+
### Database
|
|
19
|
+
- **SQLite3** for development (production-ready with proper config)
|
|
20
|
+
- Separate databases for queue, cache, and cable (see `db/*_schema.rb`)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Application Architecture
|
|
25
|
+
|
|
26
|
+
### MVC Patterns
|
|
27
|
+
|
|
28
|
+
**Models** (`app/models/`)
|
|
29
|
+
- Inherit from `ApplicationRecord` (abstract base class)
|
|
30
|
+
- Keep models focused on data and associations
|
|
31
|
+
- Use validations, scopes, and callbacks appropriately
|
|
32
|
+
- Extract complex business logic to service objects
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Pattern: Lean models with clear responsibilities
|
|
36
|
+
class Content < ApplicationRecord
|
|
37
|
+
belongs_to :user
|
|
38
|
+
has_many :versions, dependent: :destroy
|
|
39
|
+
|
|
40
|
+
validates :title, presence: true
|
|
41
|
+
validates :status, inclusion: { in: %w[draft published] }
|
|
42
|
+
|
|
43
|
+
scope :published, -> { where(status: "published") }
|
|
44
|
+
scope :by_user, ->(user) { where(user: user) }
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Controllers** (`app/controllers/`)
|
|
49
|
+
- Inherit from `ApplicationController`
|
|
50
|
+
- Use `allow_browser versions: :modern` (Rails 8.1 default)
|
|
51
|
+
- Keep actions thin, delegate to models/services
|
|
52
|
+
- Use strong parameters for all user input
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Pattern: RESTful actions with strong parameters
|
|
56
|
+
class ContentsController < ApplicationController
|
|
57
|
+
def create
|
|
58
|
+
@content = Current.user.contents.build(content_params)
|
|
59
|
+
if @content.save
|
|
60
|
+
redirect_to @content, notice: "Created successfully"
|
|
61
|
+
else
|
|
62
|
+
render :new, status: :unprocessable_entity
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def content_params
|
|
69
|
+
params.require(:content).permit(:title, :body, :status)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Views** (`app/views/`)
|
|
75
|
+
- Use Hotwire/Turbo for dynamic updates
|
|
76
|
+
- Prefer partials for reusable components
|
|
77
|
+
- Use Stimulus controllers for JavaScript behavior
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Business Logic Patterns
|
|
82
|
+
|
|
83
|
+
### Service Objects
|
|
84
|
+
Place complex operations in `app/services/`. Use when logic spans multiple models or involves external services.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# app/services/content_generator.rb
|
|
88
|
+
class ContentGenerator
|
|
89
|
+
def initialize(user:, prompt:)
|
|
90
|
+
@user = user
|
|
91
|
+
@prompt = prompt
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def call
|
|
95
|
+
# Complex AI generation logic
|
|
96
|
+
Result.new(success: true, content: generated_content)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
attr_reader :user, :prompt
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Query Objects
|
|
106
|
+
For complex queries, use `app/queries/` or model scopes.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Pattern: Chainable scopes over query objects for simple cases
|
|
110
|
+
Content.published.by_user(user).where("created_at > ?", 1.week.ago)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Background Jobs
|
|
116
|
+
|
|
117
|
+
### Solid Queue Conventions
|
|
118
|
+
- Jobs inherit from `ApplicationJob`
|
|
119
|
+
- Configure adapter in production: `config.active_job.queue_adapter = :solid_queue`
|
|
120
|
+
- Use appropriate queue names for priority
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# app/jobs/generate_content_job.rb
|
|
124
|
+
class GenerateContentJob < ApplicationJob
|
|
125
|
+
queue_as :default
|
|
126
|
+
|
|
127
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
|
|
128
|
+
discard_on ActiveJob::DeserializationError
|
|
129
|
+
|
|
130
|
+
def perform(content_id)
|
|
131
|
+
content = Content.find(content_id)
|
|
132
|
+
ContentGenerator.new(content: content).call
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## API Patterns
|
|
140
|
+
|
|
141
|
+
### JSON Responses
|
|
142
|
+
Use Jbuilder for JSON APIs. Keep API logic in dedicated controllers.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# app/controllers/api/v1/base_controller.rb
|
|
146
|
+
module Api
|
|
147
|
+
module V1
|
|
148
|
+
class BaseController < ApplicationController
|
|
149
|
+
skip_before_action :verify_authenticity_token
|
|
150
|
+
before_action :authenticate_api_request
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Turbo Streams
|
|
157
|
+
For real-time updates within the app, prefer Turbo Streams over custom APIs.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Pattern: Broadcast updates via Turbo
|
|
161
|
+
respond_to do |format|
|
|
162
|
+
format.html { redirect_to @content }
|
|
163
|
+
format.turbo_stream
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Database Conventions
|
|
170
|
+
|
|
171
|
+
### Migrations
|
|
172
|
+
- Use descriptive, timestamped migration names
|
|
173
|
+
- Always include `null: false` for required fields
|
|
174
|
+
- Add indexes for foreign keys and frequently queried columns
|
|
175
|
+
- Use `references` with `foreign_key: true`
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Pattern: Complete migration with constraints
|
|
179
|
+
class CreateContents < ActiveRecord::Migration[8.1]
|
|
180
|
+
def change
|
|
181
|
+
create_table :contents do |t|
|
|
182
|
+
t.references :user, null: false, foreign_key: true
|
|
183
|
+
t.string :title, null: false
|
|
184
|
+
t.text :body
|
|
185
|
+
t.string :status, null: false, default: "draft"
|
|
186
|
+
t.timestamps
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
add_index :contents, [:user_id, :status]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Schema
|
|
195
|
+
- Schema file is `db/schema.rb` (default)
|
|
196
|
+
- Separate schema files for queue/cache/cable databases
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Testing Approach
|
|
201
|
+
|
|
202
|
+
### Minitest (Default)
|
|
203
|
+
- Tests in `test/` directory
|
|
204
|
+
- Use fixtures for test data
|
|
205
|
+
- Parallel test execution enabled
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# Pattern: Model test with fixtures
|
|
209
|
+
class ContentTest < ActiveSupport::TestCase
|
|
210
|
+
test "validates title presence" do
|
|
211
|
+
content = Content.new(title: nil)
|
|
212
|
+
assert_not content.valid?
|
|
213
|
+
assert_includes content.errors[:title], "can't be blank"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### System Tests
|
|
219
|
+
- Use Capybara with Selenium
|
|
220
|
+
- Test user flows end-to-end
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# test/system/contents_test.rb
|
|
224
|
+
class ContentsTest < ApplicationSystemTestCase
|
|
225
|
+
test "creating a content" do
|
|
226
|
+
visit new_content_path
|
|
227
|
+
fill_in "Title", with: "Test Content"
|
|
228
|
+
click_on "Create"
|
|
229
|
+
assert_text "Created successfully"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Security Practices
|
|
237
|
+
|
|
238
|
+
### Built-in Protections
|
|
239
|
+
- CSRF protection enabled by default
|
|
240
|
+
- Strong parameters required
|
|
241
|
+
- Parameter filtering for sensitive data (see `filter_parameter_logging.rb`)
|
|
242
|
+
|
|
243
|
+
### Credentials
|
|
244
|
+
- Use `bin/rails credentials:edit` for secrets
|
|
245
|
+
- Never commit unencrypted secrets
|
|
246
|
+
- Access via `Rails.application.credentials.dig(:key, :subkey)`
|
|
247
|
+
|
|
248
|
+
### Security Tools
|
|
249
|
+
- **Brakeman**: Static security analysis (`bundle exec brakeman`)
|
|
250
|
+
- **bundler-audit**: Gem vulnerability scanning
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Code Style
|
|
255
|
+
|
|
256
|
+
### Rubocop Configuration
|
|
257
|
+
- Using `rubocop-rails-omakase` (Rails default style)
|
|
258
|
+
- Run with `bundle exec rubocop`
|
|
259
|
+
|
|
260
|
+
### Naming Conventions
|
|
261
|
+
- Models: singular, CamelCase (`Content`, `UserProfile`)
|
|
262
|
+
- Controllers: plural, CamelCase + Controller (`ContentsController`)
|
|
263
|
+
- Tables: plural, snake_case (`contents`, `user_profiles`)
|
|
264
|
+
- Jobs: descriptive + Job (`GenerateContentJob`)
|
|
265
|
+
- Services: action-oriented (`ContentGenerator`, `WorkflowExecutor`)
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Performance
|
|
270
|
+
|
|
271
|
+
### Caching
|
|
272
|
+
- Fragment caching with Solid Cache in production
|
|
273
|
+
- Use `cache` helper in views for expensive partials
|
|
274
|
+
- Russian doll caching with `touch: true` on associations
|
|
275
|
+
|
|
276
|
+
### N+1 Prevention
|
|
277
|
+
- Use `includes` for eager loading
|
|
278
|
+
- Monitor with bullet gem in development (optional)
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# Pattern: Eager load associations
|
|
282
|
+
Content.includes(:user, :versions).published
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Directory Conventions
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
app/
|
|
291
|
+
controllers/ # Request handling
|
|
292
|
+
models/ # ActiveRecord models
|
|
293
|
+
views/ # ERB templates + Turbo Streams
|
|
294
|
+
jobs/ # Background jobs (Solid Queue)
|
|
295
|
+
services/ # Business logic (create as needed)
|
|
296
|
+
helpers/ # View helpers
|
|
297
|
+
javascript/ # Stimulus controllers
|
|
298
|
+
mailers/ # Email delivery
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Extend with `app/services/`, `app/queries/` as complexity grows.
|