rails-ai-context 4.2.2 → 4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -7
- data/SECURITY.md +1 -1
- data/docs/GUIDE.md +3 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
- data/lib/rails_ai_context/doctor.rb +8 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +1 -1
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -1
- data/lib/rails_ai_context/serializers/claude_serializer.rb +32 -12
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +2 -1
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +3 -2
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +2 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +6 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +159 -64
- data/lib/rails_ai_context/server.rb +5 -1
- data/lib/rails_ai_context/tools/diagnose.rb +309 -0
- data/lib/rails_ai_context/tools/generate_test.rb +519 -0
- data/lib/rails_ai_context/tools/get_context.rb +3 -3
- data/lib/rails_ai_context/tools/onboard.rb +453 -0
- data/lib/rails_ai_context/tools/query.rb +13 -1
- data/lib/rails_ai_context/tools/review_changes.rb +290 -0
- data/lib/rails_ai_context/tools/search_code.rb +6 -3
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +8 -4
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Tools
|
|
5
|
+
class Onboard < BaseTool
|
|
6
|
+
tool_name "rails_onboard"
|
|
7
|
+
description "Get a narrative walkthrough of the Rails application — stack, data model, authentication, key flows, " \
|
|
8
|
+
"background jobs, frontend, testing, and getting started instructions. " \
|
|
9
|
+
"Use when: first encountering a project, onboarding a new developer, or orienting an AI agent. " \
|
|
10
|
+
"Key params: detail (quick/standard/full)."
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
properties: {
|
|
14
|
+
detail: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: %w[quick standard full],
|
|
17
|
+
description: "Detail level. quick: 1-paragraph overview. standard: structured walkthrough (default). full: comprehensive with all subsystems."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
23
|
+
|
|
24
|
+
def self.call(detail: "standard", server_context: nil)
|
|
25
|
+
ctx = cached_context
|
|
26
|
+
|
|
27
|
+
case detail
|
|
28
|
+
when "quick"
|
|
29
|
+
text_response(compose_quick(ctx))
|
|
30
|
+
when "full"
|
|
31
|
+
text_response(compose_full(ctx))
|
|
32
|
+
else
|
|
33
|
+
text_response(compose_standard(ctx))
|
|
34
|
+
end
|
|
35
|
+
rescue => e
|
|
36
|
+
text_response("Onboard error: #{e.message}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# ── Quick: single paragraph ──────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def compose_quick(ctx)
|
|
45
|
+
app = ctx[:app_name] || "This Rails app"
|
|
46
|
+
parts = [ "**#{app}** is a Rails #{ctx[:rails_version]} application running Ruby #{ctx[:ruby_version]}" ]
|
|
47
|
+
|
|
48
|
+
schema = ctx[:schema]
|
|
49
|
+
if schema.is_a?(Hash) && !schema[:error]
|
|
50
|
+
parts << "on #{schema[:adapter]} with #{schema[:total_tables]} tables"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
models = ctx[:models]
|
|
54
|
+
if models.is_a?(Hash) && !models[:error] && models.any?
|
|
55
|
+
top = central_models(models, 3).join(", ")
|
|
56
|
+
parts << "— #{models.size} models (key: #{top})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
tests = ctx[:tests]
|
|
60
|
+
if tests.is_a?(Hash) && !tests[:error]
|
|
61
|
+
parts << "— tested with #{tests[:framework] || 'unknown framework'}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
parts.join(" ") + "."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ── Standard: structured walkthrough ─────────────────────────────
|
|
68
|
+
|
|
69
|
+
def compose_standard(ctx) # rubocop:disable Metrics
|
|
70
|
+
lines = [ "# Welcome to #{ctx[:app_name] || 'This Rails App'}", "" ]
|
|
71
|
+
lines.concat(section_stack(ctx))
|
|
72
|
+
lines.concat(section_data_model(ctx))
|
|
73
|
+
lines.concat(section_auth(ctx))
|
|
74
|
+
lines.concat(section_key_flows(ctx))
|
|
75
|
+
lines.concat(section_jobs(ctx))
|
|
76
|
+
lines.concat(section_frontend(ctx))
|
|
77
|
+
lines.concat(section_testing(ctx))
|
|
78
|
+
lines.concat(section_getting_started(ctx))
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ── Full: standard + all subsystems ──────────────────────────────
|
|
83
|
+
|
|
84
|
+
def compose_full(ctx) # rubocop:disable Metrics
|
|
85
|
+
lines = [ "# Welcome to #{ctx[:app_name] || 'This Rails App'} (Full Walkthrough)", "" ]
|
|
86
|
+
lines.concat(section_stack(ctx))
|
|
87
|
+
lines.concat(section_data_model(ctx))
|
|
88
|
+
lines.concat(section_auth(ctx))
|
|
89
|
+
lines.concat(section_key_flows(ctx))
|
|
90
|
+
lines.concat(section_jobs(ctx))
|
|
91
|
+
lines.concat(section_frontend(ctx))
|
|
92
|
+
lines.concat(section_payments(ctx))
|
|
93
|
+
lines.concat(section_realtime(ctx))
|
|
94
|
+
lines.concat(section_storage(ctx))
|
|
95
|
+
lines.concat(section_api(ctx))
|
|
96
|
+
lines.concat(section_devops(ctx))
|
|
97
|
+
lines.concat(section_i18n(ctx))
|
|
98
|
+
lines.concat(section_engines(ctx))
|
|
99
|
+
lines.concat(section_env(ctx))
|
|
100
|
+
lines.concat(section_testing(ctx))
|
|
101
|
+
lines.concat(section_getting_started(ctx))
|
|
102
|
+
lines.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── Section builders ─────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def section_stack(ctx)
|
|
108
|
+
lines = [ "## Stack", "" ]
|
|
109
|
+
schema = ctx[:schema]
|
|
110
|
+
db = schema.is_a?(Hash) && !schema[:error] ? "#{schema[:adapter]} (#{schema[:total_tables]} tables)" : "unknown"
|
|
111
|
+
lines << "#{ctx[:app_name]} is a Rails #{ctx[:rails_version]} application running Ruby #{ctx[:ruby_version]} on #{db}."
|
|
112
|
+
|
|
113
|
+
gems = ctx[:gems]
|
|
114
|
+
if gems.is_a?(Hash) && !gems[:error]
|
|
115
|
+
notable = gems[:notable_gems] || []
|
|
116
|
+
if notable.any?
|
|
117
|
+
by_cat = notable.group_by { |g| g[:category]&.to_s || "other" }
|
|
118
|
+
gem_parts = by_cat.first(5).map { |cat, list| "#{cat}: #{list.map { |g| g[:name] }.join(', ')}" }
|
|
119
|
+
lines << "Notable gems — #{gem_parts.join('; ')}."
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
conv = ctx[:conventions]
|
|
124
|
+
if conv.is_a?(Hash) && !conv[:error]
|
|
125
|
+
arch = conv[:architecture] || []
|
|
126
|
+
lines << "Architecture: #{arch.join(', ')}." if arch.any?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
lines << ""
|
|
130
|
+
lines
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def section_data_model(ctx)
|
|
134
|
+
models = ctx[:models]
|
|
135
|
+
return [] unless models.is_a?(Hash) && !models[:error] && models.any?
|
|
136
|
+
|
|
137
|
+
lines = [ "## Data Model", "" ]
|
|
138
|
+
top = central_models(models, 7)
|
|
139
|
+
lines << "The app has #{models.size} models. The central ones are:"
|
|
140
|
+
lines << ""
|
|
141
|
+
|
|
142
|
+
top.each do |name|
|
|
143
|
+
data = models[name]
|
|
144
|
+
next unless data.is_a?(Hash) && !data[:error]
|
|
145
|
+
assocs = (data[:associations] || []).map { |a| "#{a[:type]} :#{a[:name]}" }
|
|
146
|
+
val_count = (data[:validations] || []).size
|
|
147
|
+
desc = "**#{name}**"
|
|
148
|
+
desc += " (table: `#{data[:table_name]}`)" if data[:table_name]
|
|
149
|
+
desc += " — #{assocs.first(4).join(', ')}" if assocs.any?
|
|
150
|
+
desc += ", +#{assocs.size - 4} more" if assocs.size > 4
|
|
151
|
+
desc += ". #{val_count} validations." if val_count > 0
|
|
152
|
+
lines << "- #{desc}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
remaining = models.size - top.size
|
|
156
|
+
lines << "- _...and #{remaining} more models._" if remaining > 0
|
|
157
|
+
lines << ""
|
|
158
|
+
lines
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def section_auth(ctx)
|
|
162
|
+
auth = ctx[:auth]
|
|
163
|
+
return [] unless auth.is_a?(Hash) && !auth[:error]
|
|
164
|
+
|
|
165
|
+
authentication = auth[:authentication] || {}
|
|
166
|
+
authorization = auth[:authorization] || {}
|
|
167
|
+
return [] if authentication.empty? && authorization.empty?
|
|
168
|
+
|
|
169
|
+
lines = [ "## Authentication & Authorization", "" ]
|
|
170
|
+
if authentication[:method]
|
|
171
|
+
lines << "Authentication is handled by #{authentication[:method]}."
|
|
172
|
+
end
|
|
173
|
+
if authentication[:model]
|
|
174
|
+
lines << "The #{authentication[:model]} model handles user accounts."
|
|
175
|
+
end
|
|
176
|
+
if authorization[:method]
|
|
177
|
+
lines << "Authorization uses #{authorization[:method]}."
|
|
178
|
+
end
|
|
179
|
+
lines << ""
|
|
180
|
+
lines
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def section_key_flows(ctx)
|
|
184
|
+
routes = ctx[:routes]
|
|
185
|
+
controllers = ctx[:controllers]
|
|
186
|
+
return [] unless routes.is_a?(Hash) && !routes[:error]
|
|
187
|
+
|
|
188
|
+
lines = [ "## Key Flows", "" ]
|
|
189
|
+
by_ctrl = routes[:by_controller] || {}
|
|
190
|
+
|
|
191
|
+
# Find controllers with most actions (most important flows)
|
|
192
|
+
internal_prefixes = %w[action_mailbox/ active_storage/ rails/ conductor/ devise/ turbo/]
|
|
193
|
+
app_ctrls = by_ctrl.reject { |k, _| internal_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
194
|
+
top_ctrls = app_ctrls.sort_by { |_, routes_list| -routes_list.size }.first(5)
|
|
195
|
+
|
|
196
|
+
top_ctrls.each do |ctrl, ctrl_routes|
|
|
197
|
+
actions = ctrl_routes.map { |r| r[:action] }.compact.uniq
|
|
198
|
+
verbs = ctrl_routes.map { |r| "#{r[:verb]} #{r[:path]}" }.first(3)
|
|
199
|
+
lines << "- **#{ctrl}** — #{actions.join(', ')} (#{verbs.join(', ')})"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
lines << ""
|
|
203
|
+
lines << "Total: #{routes[:total_routes]} routes across #{app_ctrls.size} controllers."
|
|
204
|
+
lines << ""
|
|
205
|
+
lines
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def section_jobs(ctx)
|
|
209
|
+
jobs = ctx[:jobs]
|
|
210
|
+
return [] unless jobs.is_a?(Hash) && !jobs[:error]
|
|
211
|
+
|
|
212
|
+
job_list = jobs[:jobs] || []
|
|
213
|
+
mailers = jobs[:mailers] || []
|
|
214
|
+
channels = jobs[:channels] || []
|
|
215
|
+
return [] if job_list.empty? && mailers.empty? && channels.empty?
|
|
216
|
+
|
|
217
|
+
lines = [ "## Background Jobs & Async", "" ]
|
|
218
|
+
if job_list.any?
|
|
219
|
+
names = job_list.map { |j| j[:name] || j[:class_name] }.compact.first(8)
|
|
220
|
+
lines << "#{job_list.size} background jobs: #{names.join(', ')}#{job_list.size > 8 ? ', ...' : ''}."
|
|
221
|
+
end
|
|
222
|
+
lines << "#{mailers.size} mailers." if mailers.any?
|
|
223
|
+
lines << "#{channels.size} Action Cable channels." if channels.any?
|
|
224
|
+
lines << ""
|
|
225
|
+
lines
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def section_frontend(ctx)
|
|
229
|
+
frontend = ctx[:frontend_frameworks]
|
|
230
|
+
stimulus = ctx[:stimulus]
|
|
231
|
+
turbo = ctx[:turbo]
|
|
232
|
+
|
|
233
|
+
lines = []
|
|
234
|
+
has_content = false
|
|
235
|
+
|
|
236
|
+
if frontend.is_a?(Hash) && !frontend[:error] && frontend[:frameworks]&.any?
|
|
237
|
+
lines << "## Frontend" << ""
|
|
238
|
+
frameworks = frontend[:frameworks]
|
|
239
|
+
if frameworks.is_a?(Hash)
|
|
240
|
+
frameworks.each { |name, version| lines << "- #{name} #{version}".strip }
|
|
241
|
+
elsif frameworks.is_a?(Array)
|
|
242
|
+
frameworks.each { |fw| lines << "- #{fw.is_a?(Hash) ? "#{fw[:name]} #{fw[:version]}" : fw}".strip }
|
|
243
|
+
end
|
|
244
|
+
has_content = true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if stimulus.is_a?(Hash) && !stimulus[:error]
|
|
248
|
+
count = stimulus[:total_controllers] || stimulus[:controllers]&.size || 0
|
|
249
|
+
if count > 0
|
|
250
|
+
lines << "## Frontend" << "" unless has_content
|
|
251
|
+
lines << "Stimulus: #{count} controllers for interactive behavior."
|
|
252
|
+
has_content = true
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if turbo.is_a?(Hash) && !turbo[:error]
|
|
257
|
+
frames = turbo[:frames]&.size || 0
|
|
258
|
+
streams = turbo[:streams]&.size || 0
|
|
259
|
+
if frames > 0 || streams > 0
|
|
260
|
+
lines << "## Frontend" << "" unless has_content
|
|
261
|
+
parts = []
|
|
262
|
+
parts << "#{frames} Turbo Frames" if frames > 0
|
|
263
|
+
parts << "#{streams} Turbo Streams" if streams > 0
|
|
264
|
+
lines << "Hotwire: #{parts.join(', ')}."
|
|
265
|
+
has_content = true
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
lines << "" if has_content
|
|
270
|
+
lines
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def section_testing(ctx)
|
|
274
|
+
tests = ctx[:tests]
|
|
275
|
+
return [] unless tests.is_a?(Hash) && !tests[:error]
|
|
276
|
+
|
|
277
|
+
lines = [ "## Testing", "" ]
|
|
278
|
+
framework = tests[:framework] || "unknown"
|
|
279
|
+
lines << "Framework: #{framework}."
|
|
280
|
+
|
|
281
|
+
factories = tests[:factories]
|
|
282
|
+
fixtures = tests[:fixtures]
|
|
283
|
+
lines << "Data setup: #{factories ? "FactoryBot (#{factories[:count]} factories)" : fixtures ? "fixtures (#{fixtures[:count]} files)" : "inline"}."
|
|
284
|
+
|
|
285
|
+
ci = tests[:ci_config]
|
|
286
|
+
lines << "CI: #{ci.join(', ')}." if ci&.any?
|
|
287
|
+
|
|
288
|
+
coverage = tests[:coverage]
|
|
289
|
+
lines << "Coverage: #{coverage}." if coverage
|
|
290
|
+
|
|
291
|
+
test_cmd = framework == "rspec" ? "bundle exec rspec" : "rails test"
|
|
292
|
+
lines << "" << "Run tests: `#{test_cmd}`"
|
|
293
|
+
lines << ""
|
|
294
|
+
lines
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def section_getting_started(ctx)
|
|
298
|
+
test_cmd = (ctx[:tests].is_a?(Hash) && ctx[:tests][:framework] == "rspec") ? "bundle exec rspec" : "rails test"
|
|
299
|
+
[
|
|
300
|
+
"## Getting Started", "",
|
|
301
|
+
"```bash",
|
|
302
|
+
"git clone <repo-url>",
|
|
303
|
+
"cd #{ctx[:app_name]&.underscore || 'app'}",
|
|
304
|
+
"bundle install",
|
|
305
|
+
"rails db:setup",
|
|
306
|
+
"bin/dev # or rails server",
|
|
307
|
+
"#{test_cmd} # verify everything works",
|
|
308
|
+
"```", ""
|
|
309
|
+
]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ── Full-only sections ───────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
def section_payments(ctx)
|
|
315
|
+
gems = ctx[:gems]
|
|
316
|
+
models = ctx[:models]
|
|
317
|
+
return [] unless gems.is_a?(Hash) && !gems[:error] && models.is_a?(Hash)
|
|
318
|
+
|
|
319
|
+
payment_gems = %w[stripe pay braintree paddle_pay]
|
|
320
|
+
notable = gems[:notable_gems] || []
|
|
321
|
+
found = notable.select { |g| payment_gems.include?(g[:name]) }
|
|
322
|
+
payment_models = models.keys.select { |m| m.downcase.match?(/payment|subscription|charge|invoice|plan|billing/) }
|
|
323
|
+
return [] if found.empty? && payment_models.empty?
|
|
324
|
+
|
|
325
|
+
lines = [ "## Payments", "" ]
|
|
326
|
+
lines << "Payment gems: #{found.map { |g| g[:name] }.join(', ')}." if found.any?
|
|
327
|
+
lines << "Payment-related models: #{payment_models.join(', ')}." if payment_models.any?
|
|
328
|
+
lines << ""
|
|
329
|
+
lines
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def section_realtime(ctx)
|
|
333
|
+
turbo = ctx[:turbo]
|
|
334
|
+
jobs = ctx[:jobs]
|
|
335
|
+
channels = (jobs.is_a?(Hash) ? jobs[:channels] : nil) || []
|
|
336
|
+
return [] unless (turbo.is_a?(Hash) && !turbo[:error]) || channels.any?
|
|
337
|
+
|
|
338
|
+
lines = [ "## Real-Time Features", "" ]
|
|
339
|
+
if channels.any?
|
|
340
|
+
names = channels.map { |c| c[:name] || c[:class_name] }.compact
|
|
341
|
+
lines << "Action Cable channels: #{names.join(', ')}."
|
|
342
|
+
end
|
|
343
|
+
if turbo.is_a?(Hash) && !turbo[:error] && turbo[:broadcasts]&.any?
|
|
344
|
+
lines << "Turbo Stream broadcasts: #{turbo[:broadcasts].size} broadcast points."
|
|
345
|
+
end
|
|
346
|
+
lines << ""
|
|
347
|
+
lines
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def section_storage(ctx)
|
|
351
|
+
storage = ctx[:active_storage]
|
|
352
|
+
text = ctx[:action_text]
|
|
353
|
+
return [] unless (storage.is_a?(Hash) && !storage[:error]) || (text.is_a?(Hash) && !text[:error])
|
|
354
|
+
|
|
355
|
+
lines = [ "## File Storage & Rich Text", "" ]
|
|
356
|
+
if storage.is_a?(Hash) && !storage[:error] && storage[:attachments]&.any?
|
|
357
|
+
lines << "Active Storage: #{storage[:attachments].size} attachment(s) across models."
|
|
358
|
+
end
|
|
359
|
+
if text.is_a?(Hash) && !text[:error] && text[:models]&.any?
|
|
360
|
+
lines << "Action Text: #{text[:models].size} model(s) with rich text fields."
|
|
361
|
+
end
|
|
362
|
+
lines << ""
|
|
363
|
+
lines
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def section_api(ctx)
|
|
367
|
+
api = ctx[:api]
|
|
368
|
+
return [] unless api.is_a?(Hash) && !api[:error]
|
|
369
|
+
return [] if api.empty? || (api[:endpoints]&.empty? && api[:graphql].nil?)
|
|
370
|
+
|
|
371
|
+
lines = [ "## API", "" ]
|
|
372
|
+
if api[:graphql]
|
|
373
|
+
lines << "GraphQL API detected."
|
|
374
|
+
end
|
|
375
|
+
if api[:endpoints]&.any?
|
|
376
|
+
lines << "#{api[:endpoints].size} API endpoint(s)."
|
|
377
|
+
end
|
|
378
|
+
if api[:serializers]&.any?
|
|
379
|
+
lines << "Serializers: #{api[:serializers].size}."
|
|
380
|
+
end
|
|
381
|
+
lines << ""
|
|
382
|
+
lines
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def section_devops(ctx)
|
|
386
|
+
devops = ctx[:devops]
|
|
387
|
+
return [] unless devops.is_a?(Hash) && !devops[:error]
|
|
388
|
+
|
|
389
|
+
lines = [ "## Deployment & DevOps", "" ]
|
|
390
|
+
lines << "Dockerfile: #{devops[:dockerfile] ? 'present' : 'not found'}."
|
|
391
|
+
lines << "Procfile: #{devops[:procfile] ? 'present' : 'not found'}." if devops.key?(:procfile)
|
|
392
|
+
deploy = devops[:deployment_method]
|
|
393
|
+
lines << "Deployment: #{deploy}." if deploy
|
|
394
|
+
lines << ""
|
|
395
|
+
lines
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def section_i18n(ctx)
|
|
399
|
+
i18n = ctx[:i18n]
|
|
400
|
+
return [] unless i18n.is_a?(Hash) && !i18n[:error]
|
|
401
|
+
|
|
402
|
+
locales = i18n[:locales] || []
|
|
403
|
+
return [] if locales.empty?
|
|
404
|
+
|
|
405
|
+
[ "## Internationalization", "", "Locales: #{locales.join(', ')}.", "" ]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def section_engines(ctx)
|
|
409
|
+
engines = ctx[:engines]
|
|
410
|
+
return [] unless engines.is_a?(Hash) && !engines[:error]
|
|
411
|
+
|
|
412
|
+
mounted = engines[:engines] || engines[:mounted] || []
|
|
413
|
+
return [] if mounted.empty?
|
|
414
|
+
|
|
415
|
+
lines = [ "## Mounted Engines", "" ]
|
|
416
|
+
mounted.each do |e|
|
|
417
|
+
name = e[:name] || e[:engine]
|
|
418
|
+
path = e[:path] || e[:mount_path]
|
|
419
|
+
lines << "- **#{name}** at `#{path}`" if name
|
|
420
|
+
end
|
|
421
|
+
lines << ""
|
|
422
|
+
lines
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def section_env(ctx)
|
|
426
|
+
# Summarize from models that have encrypts, and auth/payment-related env patterns
|
|
427
|
+
models = ctx[:models]
|
|
428
|
+
return [] unless models.is_a?(Hash) && !models[:error]
|
|
429
|
+
|
|
430
|
+
encrypted = models.select { |_, d| d.is_a?(Hash) && d[:encrypts]&.any? }
|
|
431
|
+
return [] if encrypted.empty?
|
|
432
|
+
|
|
433
|
+
lines = [ "## Encrypted Data", "" ]
|
|
434
|
+
encrypted.each do |name, data|
|
|
435
|
+
lines << "- **#{name}**: encrypts #{data[:encrypts].join(', ')}"
|
|
436
|
+
end
|
|
437
|
+
lines << ""
|
|
438
|
+
lines
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# ── Helpers ──────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
def central_models(models, limit = 5)
|
|
444
|
+
models
|
|
445
|
+
.select { |_, d| d.is_a?(Hash) && !d[:error] }
|
|
446
|
+
.sort_by { |_, d| -(d[:associations]&.size || 0) }
|
|
447
|
+
.first(limit)
|
|
448
|
+
.map(&:first)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
@@ -175,12 +175,24 @@ module RailsAiContext
|
|
|
175
175
|
|
|
176
176
|
private_class_method def self.execute_sqlite(conn, sql, timeout)
|
|
177
177
|
raw = conn.raw_connection
|
|
178
|
-
raw.busy_timeout = (timeout * 1000).to_i
|
|
179
178
|
result = nil
|
|
180
179
|
begin
|
|
181
180
|
conn.execute("PRAGMA query_only = ON")
|
|
181
|
+
# SQLite has no native statement timeout. Use a progress handler
|
|
182
|
+
# to abort queries that run too long (checked every 1000 VM steps).
|
|
183
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
184
|
+
if raw.respond_to?(:set_progress_handler)
|
|
185
|
+
raw.set_progress_handler(1000) do
|
|
186
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
187
|
+
1 # non-zero = abort
|
|
188
|
+
else
|
|
189
|
+
0
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
182
193
|
result = conn.select_all(sql)
|
|
183
194
|
ensure
|
|
195
|
+
raw.set_progress_handler(0, nil) if raw.respond_to?(:set_progress_handler)
|
|
184
196
|
conn.execute("PRAGMA query_only = OFF")
|
|
185
197
|
end
|
|
186
198
|
result
|