rails-ai-context 1.1.1 → 1.2.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 +14 -0
- data/CLAUDE.md +3 -2
- data/SECURITY.md +1 -0
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +65 -1
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +214 -5
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +9 -25
- data/lib/rails_ai_context/serializers/claude_serializer.rb +4 -30
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +4 -29
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +3 -10
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +6 -18
- data/lib/rails_ai_context/serializers/design_system_helper.rb +233 -0
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +4 -17
- data/lib/rails_ai_context/serializers/windsurf_rules_serializer.rb +7 -40
- data/lib/rails_ai_context/serializers/windsurf_serializer.rb +5 -31
- data/lib/rails_ai_context/server.rb +2 -1
- data/lib/rails_ai_context/tools/get_design_system.rb +281 -0
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e7442b6a28d089867479cba4a99d6f0fcf64e34f692d8c47b576de8e7ec4ffd
|
|
4
|
+
data.tar.gz: 0b8b21e8c1581a056afbb03e3ef6b3c45a62c7fbb61d345bbec49d370c4fd191
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1bd3b280678917a3b94576cef271669fc6892ca32cfd522b88a4c7cee93d73d4301b6d995bd75e78fd64bb448f5e226013b8678411bfd56e68de0a59ccba35b
|
|
7
|
+
data.tar.gz: a57fed3f44c4e1599854caf8243a35f62476f487e8b14a41489bcaee1efb4e8398935211ea9073ca70f9a1e34c16a6fa13d79cdaa5cf943d6f0d5d2e82ea100c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Design system extraction** — ViewTemplateIntrospector now extracts canonical page examples (real HTML/ERB snippets from actual views), full color palette with semantic roles (primary/danger/success/warning), typography scale (sizes, weights, heading styles), layout patterns (containers, grids, spacing scale), responsive breakpoint usage, interactive state patterns (hover/focus/active/disabled), dark mode detection, and icon system identification.
|
|
13
|
+
- **New MCP tool: `rails_get_design_system`** — dedicated tool (15th) returns the app's design system: color palette, component patterns with real HTML examples, typography, layout conventions, responsive breakpoints. Supports `detail` parameter (summary/standard/full). Total MCP tools: 15.
|
|
14
|
+
- **DesignSystemHelper serializer module** — replaces flat component listings with actionable design guidance across all output formats (Claude, Cursor, Windsurf, Copilot, OpenCode). Shows components with semantic roles, canonical page examples in split rules, and explicit design rules.
|
|
15
|
+
- **DesignTokenIntrospector semantic categorization** — tokens now grouped into colors/typography/spacing/sizing/borders/shadows. Enhanced Tailwind v3 parsing for fontSize, spacing, borderRadius, and screens.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **"UI Patterns" section renamed to "Design System"** — richer content with color palette, typography, components, spacing conventions, interactive states, and design rules.
|
|
20
|
+
- **Design tokens consumed for the first time** — `context[:design_tokens]` data was previously extracted but never rendered. Now merged into design system output in all serializers and the new MCP tool.
|
|
21
|
+
|
|
8
22
|
## [1.1.1] - 2026-03-23
|
|
9
23
|
|
|
10
24
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
9
9
|
- `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
11
11
|
- `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
|
|
12
|
-
- `lib/rails_ai_context/tools/` —
|
|
12
|
+
- `lib/rails_ai_context/tools/` — 15 MCP tools using the official mcp SDK
|
|
13
13
|
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection)
|
|
14
14
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
@@ -39,7 +39,8 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
39
39
|
13. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
|
|
40
40
|
14. **Section markers** — root file content wrapped in `<!-- BEGIN/END rails-ai-context -->` to preserve user content
|
|
41
41
|
15. **generate_root_files toggle** — when false, skip root files (CLAUDE.md, etc.), only generate split rules
|
|
42
|
-
16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the
|
|
42
|
+
16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the 15 built-in tools
|
|
43
|
+
17. **Design system extraction** — view templates analyzed for canonical examples, color palette, typography, responsive patterns, interactive states, dark mode
|
|
43
44
|
|
|
44
45
|
## Testing
|
|
45
46
|
|
data/SECURITY.md
CHANGED
|
@@ -37,7 +37,8 @@ module RailsAiContext
|
|
|
37
37
|
|
|
38
38
|
{
|
|
39
39
|
framework: detect_framework(root),
|
|
40
|
-
tokens: tokens
|
|
40
|
+
tokens: tokens,
|
|
41
|
+
categorized: categorize_tokens(tokens)
|
|
41
42
|
}
|
|
42
43
|
rescue => e
|
|
43
44
|
{ error: e.message }
|
|
@@ -116,6 +117,34 @@ module RailsAiContext
|
|
|
116
117
|
content.scan(/(\w+)\s*:\s*\[['"]([^'"]+)['"]/).each do |name, font|
|
|
117
118
|
tokens["tw3-font-#{name}"] = font if name.match?(/font|sans|serif|mono|display|heading/)
|
|
118
119
|
end
|
|
120
|
+
|
|
121
|
+
# Extract fontSize configuration
|
|
122
|
+
content.scan(/fontSize\s*:\s*\{([^}]+)\}/m).each do |match|
|
|
123
|
+
match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
|
|
124
|
+
tokens["tw3-fontSize-#{name}"] = value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Extract screens (breakpoints)
|
|
129
|
+
content.scan(/screens\s*:\s*\{([^}]+)\}/m).each do |match|
|
|
130
|
+
match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
|
|
131
|
+
tokens["tw3-screen-#{name}"] = value
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Extract spacing overrides
|
|
136
|
+
content.scan(/spacing\s*:\s*\{([^}]+)\}/m).each do |match|
|
|
137
|
+
match[0].scan(/['"]([\w.-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
|
|
138
|
+
tokens["tw3-spacing-#{name}"] = value
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Extract borderRadius overrides
|
|
143
|
+
content.scan(/borderRadius\s*:\s*\{([^}]+)\}/m).each do |match|
|
|
144
|
+
match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
|
|
145
|
+
tokens["tw3-radius-#{name}"] = value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
119
148
|
end
|
|
120
149
|
|
|
121
150
|
# 4. Bootstrap/Sass variable definitions
|
|
@@ -174,6 +203,41 @@ module RailsAiContext
|
|
|
174
203
|
end
|
|
175
204
|
end
|
|
176
205
|
|
|
206
|
+
def categorize_tokens(tokens)
|
|
207
|
+
categories = {
|
|
208
|
+
colors: {},
|
|
209
|
+
typography: {},
|
|
210
|
+
spacing: {},
|
|
211
|
+
sizing: {},
|
|
212
|
+
borders: {},
|
|
213
|
+
shadows: {},
|
|
214
|
+
other: {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
tokens.each do |name, value|
|
|
218
|
+
category = case name
|
|
219
|
+
when /color|brand|primary|secondary|danger|success|warning|accent|neutral|bg|surface/i
|
|
220
|
+
:colors
|
|
221
|
+
when /font|text-size|leading|tracking|letter-spacing|line-height/i
|
|
222
|
+
:typography
|
|
223
|
+
when /spacing|gap|margin|padding|space|inset/i
|
|
224
|
+
:spacing
|
|
225
|
+
when /width|height|size|radius|rounded|screen|breakpoint/i
|
|
226
|
+
:sizing
|
|
227
|
+
when /border|ring|outline|divide/i
|
|
228
|
+
:borders
|
|
229
|
+
when /shadow/i
|
|
230
|
+
:shadows
|
|
231
|
+
else
|
|
232
|
+
name.match?(/shade-\d+|#[\da-fA-F]/) ? :colors : :other
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
categories[category][name] = value
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
categories.reject { |_, v| v.empty? }
|
|
239
|
+
end
|
|
240
|
+
|
|
177
241
|
# Helper: extract :root { --var: value } from CSS content
|
|
178
242
|
def extract_root_vars(content, tokens)
|
|
179
243
|
content.scan(/:root\s*(?:,\s*:host)?\s*\{([^}]+)\}/m).each do |match|
|
|
@@ -20,7 +20,9 @@ module RailsAiContext
|
|
|
20
20
|
{
|
|
21
21
|
templates: scan_templates(views_dir),
|
|
22
22
|
partials: scan_partials(views_dir),
|
|
23
|
-
ui_patterns: extract_ui_patterns(all_content)
|
|
23
|
+
ui_patterns: extract_ui_patterns(all_content).merge(
|
|
24
|
+
canonical_examples: extract_canonical_examples(views_dir)
|
|
25
|
+
)
|
|
24
26
|
}
|
|
25
27
|
rescue => e
|
|
26
28
|
{ error: e.message }
|
|
@@ -106,7 +108,13 @@ module RailsAiContext
|
|
|
106
108
|
color_scheme: color_scheme,
|
|
107
109
|
radius: radius_convention,
|
|
108
110
|
form_layout: form_layout,
|
|
109
|
-
components: components
|
|
111
|
+
components: components,
|
|
112
|
+
typography: extract_typography(all_content),
|
|
113
|
+
layout: extract_layout_patterns(all_content),
|
|
114
|
+
responsive: extract_responsive_patterns(all_content),
|
|
115
|
+
interactive_states: extract_interactive_states(all_content),
|
|
116
|
+
dark_mode: extract_dark_mode_patterns(all_content),
|
|
117
|
+
icons: extract_icon_system(all_content)
|
|
110
118
|
}
|
|
111
119
|
end
|
|
112
120
|
|
|
@@ -149,12 +157,19 @@ module RailsAiContext
|
|
|
149
157
|
"primary"
|
|
150
158
|
end
|
|
151
159
|
next unless role # skip unclassified buttons
|
|
152
|
-
classified[role]
|
|
160
|
+
classified[role] ||= { classes: c, count: count, variants: [] }
|
|
161
|
+
classified[role][:variants] << { classes: c, count: count }
|
|
162
|
+
if count > classified[role][:count]
|
|
163
|
+
classified[role][:classes] = c
|
|
164
|
+
classified[role][:count] = count
|
|
165
|
+
end
|
|
153
166
|
end
|
|
154
167
|
|
|
155
168
|
classified.each do |role, data|
|
|
156
169
|
label = role == "default" ? "Button" : "Button (#{role})"
|
|
157
|
-
|
|
170
|
+
entry = { type: :button, label: label, classes: data[:classes] }
|
|
171
|
+
entry[:variants] = data[:variants] if data[:variants]&.size&.> 1
|
|
172
|
+
components << entry
|
|
158
173
|
used << data[:classes]
|
|
159
174
|
end
|
|
160
175
|
end
|
|
@@ -287,7 +302,7 @@ module RailsAiContext
|
|
|
287
302
|
components << { type: :alert, label: "Alert", classes: best[0] }
|
|
288
303
|
end
|
|
289
304
|
|
|
290
|
-
def extract_color_scheme(
|
|
305
|
+
def extract_color_scheme(content, groups)
|
|
291
306
|
# Find primary color from button backgrounds
|
|
292
307
|
primary_colors = Hash.new(0)
|
|
293
308
|
groups.each do |c, count|
|
|
@@ -306,6 +321,27 @@ module RailsAiContext
|
|
|
306
321
|
scheme = {}
|
|
307
322
|
scheme[:primary] = primary if primary
|
|
308
323
|
scheme[:text] = text_colors.join("/") if text_colors.any?
|
|
324
|
+
|
|
325
|
+
# Full palette: background colors used
|
|
326
|
+
bg_colors = Hash.new(0)
|
|
327
|
+
content.scan(/bg-(\w+)-(\d+)/).each { |color, shade| bg_colors["#{color}-#{shade}"] += 1 }
|
|
328
|
+
scheme[:background_palette] = bg_colors.sort_by { |_, c| -c }.first(10).map(&:first) if bg_colors.any?
|
|
329
|
+
|
|
330
|
+
# Text color palette
|
|
331
|
+
text_palette = Hash.new(0)
|
|
332
|
+
content.scan(/text-(\w+)-(\d+)/).each { |color, shade| text_palette["#{color}-#{shade}"] += 1 }
|
|
333
|
+
scheme[:text_palette] = text_palette.sort_by { |_, c| -c }.first(8).map(&:first) if text_palette.any?
|
|
334
|
+
|
|
335
|
+
# Border color palette
|
|
336
|
+
border_colors = Hash.new(0)
|
|
337
|
+
content.scan(/border-(\w+)-(\d+)/).each { |color, shade| border_colors["#{color}-#{shade}"] += 1 }
|
|
338
|
+
scheme[:border_palette] = border_colors.sort_by { |_, c| -c }.first(5).map(&:first) if border_colors.any?
|
|
339
|
+
|
|
340
|
+
# Semantic roles inferred from usage
|
|
341
|
+
scheme[:danger] = "red" if bg_colors.any? { |k, _| k.start_with?("red-") }
|
|
342
|
+
scheme[:success] = "green" if bg_colors.any? { |k, _| k.start_with?("green-") }
|
|
343
|
+
scheme[:warning] = "yellow" if bg_colors.any? { |k, _| k.start_with?("yellow-") || k.start_with?("amber-") }
|
|
344
|
+
|
|
309
345
|
scheme
|
|
310
346
|
end
|
|
311
347
|
|
|
@@ -332,6 +368,179 @@ module RailsAiContext
|
|
|
332
368
|
layout
|
|
333
369
|
end
|
|
334
370
|
|
|
371
|
+
def extract_responsive_patterns(content)
|
|
372
|
+
breakpoints = {}
|
|
373
|
+
%w[sm md lg xl 2xl].each do |bp|
|
|
374
|
+
matches = content.scan(/#{bp}:[\w-]+/).map { |m| m.sub("#{bp}:", "") }
|
|
375
|
+
next if matches.empty?
|
|
376
|
+
breakpoints[bp] = matches.tally.sort_by { |_, c| -c }.first(5).to_h
|
|
377
|
+
end
|
|
378
|
+
breakpoints
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def extract_interactive_states(content)
|
|
382
|
+
states = {}
|
|
383
|
+
%w[hover focus active disabled group-hover focus-within focus-visible].each do |state|
|
|
384
|
+
matches = content.scan(/#{state}:[\w-]+/)
|
|
385
|
+
next if matches.empty?
|
|
386
|
+
states[state] = matches.tally.sort_by { |_, c| -c }.first(5).to_h
|
|
387
|
+
end
|
|
388
|
+
states
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def extract_dark_mode_patterns(content)
|
|
392
|
+
dark_classes = content.scan(/dark:[\w-]+/)
|
|
393
|
+
return {} if dark_classes.empty?
|
|
394
|
+
{ used: true, patterns: dark_classes.tally.sort_by { |_, c| -c }.first(10).to_h }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def extract_layout_patterns(content)
|
|
398
|
+
layout = {}
|
|
399
|
+
|
|
400
|
+
containers = content.scan(/(?:max-w-\w+|container)\b/).tally
|
|
401
|
+
layout[:containers] = containers.sort_by { |_, c| -c }.first(3).to_h unless containers.empty?
|
|
402
|
+
|
|
403
|
+
flex = content.scan(/(?:flex-(?:row|col|wrap)|items-\w+|justify-\w+)\b/).tally
|
|
404
|
+
layout[:flex] = flex.sort_by { |_, c| -c }.first(5).to_h unless flex.empty?
|
|
405
|
+
|
|
406
|
+
grids = content.scan(/grid-cols-\d+/).tally
|
|
407
|
+
layout[:grid] = grids.sort_by { |_, c| -c }.first(3).to_h unless grids.empty?
|
|
408
|
+
|
|
409
|
+
spacing = content.scan(/(?:space-[xy]-|gap-|p-|px-|py-|m-|mx-|my-|mt-|mb-)\d+/).tally
|
|
410
|
+
layout[:spacing_scale] = spacing.sort_by { |_, c| -c }.first(8).to_h unless spacing.empty?
|
|
411
|
+
|
|
412
|
+
layout
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def extract_typography(content)
|
|
416
|
+
typo = {}
|
|
417
|
+
|
|
418
|
+
sizes = content.scan(/text-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)/).tally
|
|
419
|
+
typo[:sizes] = sizes.sort_by { |_, c| -c }.to_h unless sizes.empty?
|
|
420
|
+
|
|
421
|
+
weights = content.scan(/font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)/).tally
|
|
422
|
+
typo[:weights] = weights.sort_by { |_, c| -c }.to_h unless weights.empty?
|
|
423
|
+
|
|
424
|
+
headings = {}
|
|
425
|
+
%w[h1 h2 h3 h4].each do |tag|
|
|
426
|
+
classes = content.scan(/<#{tag}[^>]*class=["']([^"']+)["']/i).map { |m| m[0].gsub(/<%=.*?%>/, "").strip }
|
|
427
|
+
next if classes.empty?
|
|
428
|
+
headings[tag] = classes.tally.max_by { |_, c| c }[0]
|
|
429
|
+
end
|
|
430
|
+
typo[:heading_styles] = headings unless headings.empty?
|
|
431
|
+
|
|
432
|
+
leading = content.scan(/leading-\w+/).tally
|
|
433
|
+
typo[:line_height] = leading.sort_by { |_, c| -c }.first(3).to_h unless leading.empty?
|
|
434
|
+
|
|
435
|
+
typo
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def extract_icon_system(content)
|
|
439
|
+
icons = {}
|
|
440
|
+
|
|
441
|
+
icons[:library] = "heroicons" if content.match?(/heroicon|hero_icon/)
|
|
442
|
+
icons[:library] = "lucide" if content.match?(/lucide|data-lucide/)
|
|
443
|
+
icons[:library] = "font-awesome" if content.match?(/fa-\w+|font-awesome/)
|
|
444
|
+
icons[:library] = "bootstrap-icons" if content.match?(/bi-\w+/)
|
|
445
|
+
|
|
446
|
+
svg_count = content.scan(/<svg\b/).size
|
|
447
|
+
icons[:inline_svg_count] = svg_count if svg_count > 0
|
|
448
|
+
|
|
449
|
+
icon_sizes = content.scan(/(?:w-\d+\s+h-\d+|size-\d+)/).tally
|
|
450
|
+
icons[:sizes] = icon_sizes.sort_by { |_, c| -c }.first(3).to_h unless icon_sizes.empty?
|
|
451
|
+
|
|
452
|
+
icons.empty? ? nil : icons
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Analyzes individual templates to find canonical examples of common page types.
|
|
456
|
+
# Returns up to 5 representative ERB snippets that AI can copy.
|
|
457
|
+
def extract_canonical_examples(views_dir) # rubocop:disable Metrics
|
|
458
|
+
max_snippet = 80 # lines per example
|
|
459
|
+
max_file = RailsAiContext.configuration.max_view_file_size
|
|
460
|
+
examples = {}
|
|
461
|
+
|
|
462
|
+
Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).each do |path|
|
|
463
|
+
next if File.directory?(path)
|
|
464
|
+
next if File.basename(path).start_with?("_") # skip partials
|
|
465
|
+
next if path.include?("/layouts/")
|
|
466
|
+
next if File.size(path) > max_file
|
|
467
|
+
|
|
468
|
+
relative = path.sub("#{views_dir}/", "")
|
|
469
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
470
|
+
|
|
471
|
+
page_type = classify_template(content)
|
|
472
|
+
next unless page_type
|
|
473
|
+
|
|
474
|
+
score = score_template(content)
|
|
475
|
+
existing = examples[page_type]
|
|
476
|
+
|
|
477
|
+
if !existing || score > existing[:score]
|
|
478
|
+
snippet = content.lines.first(max_snippet).join
|
|
479
|
+
# Strip large SVG blocks
|
|
480
|
+
snippet = snippet.gsub(/<svg[^>]*>.*?<\/svg>/m, "<!-- svg icon -->")
|
|
481
|
+
components_used = detect_components_in_template(content)
|
|
482
|
+
|
|
483
|
+
examples[page_type] = {
|
|
484
|
+
type: page_type,
|
|
485
|
+
template: relative,
|
|
486
|
+
snippet: snippet,
|
|
487
|
+
components_used: components_used,
|
|
488
|
+
score: score
|
|
489
|
+
}
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
examples.values.map { |e| e.except(:score) }.first(5)
|
|
494
|
+
rescue
|
|
495
|
+
[]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def classify_template(content)
|
|
499
|
+
has_form = content.match?(/form_with|form_for|<form\b/i)
|
|
500
|
+
has_collection = content.match?(/\.each\s+do\b|render\s+collection:|render\s+@\w+/)
|
|
501
|
+
has_grid = content.match?(/grid-cols-|grid\s+grid-/)
|
|
502
|
+
has_show = content.match?(/\A(?:(?!\.each).)*@\w+\.\w+/m) && !has_collection
|
|
503
|
+
|
|
504
|
+
if has_form && !has_collection
|
|
505
|
+
:form_page
|
|
506
|
+
elsif has_collection || has_grid
|
|
507
|
+
:list_page
|
|
508
|
+
elsif has_show
|
|
509
|
+
:show_page
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def score_template(content)
|
|
514
|
+
score = 0
|
|
515
|
+
# Prefer templates with more design patterns
|
|
516
|
+
score += content.scan(/class=["'][^"']+["']/).size.clamp(0, 20)
|
|
517
|
+
# Responsive classes
|
|
518
|
+
score += 5 if content.match?(/(?:sm|md|lg|xl):/)
|
|
519
|
+
# Interactive states
|
|
520
|
+
score += 3 if content.match?(/hover:|focus:/)
|
|
521
|
+
# Component variety
|
|
522
|
+
score += 2 if content.match?(/<button|btn/i)
|
|
523
|
+
score += 2 if content.match?(/shadow.*rounded|card/i)
|
|
524
|
+
score += 2 if content.match?(/<input|form_with|form_for/i)
|
|
525
|
+
# Not too short, not too long (sweet spot: 30-100 lines)
|
|
526
|
+
lines = content.lines.size
|
|
527
|
+
score += 5 if lines.between?(30, 100)
|
|
528
|
+
score -= 3 if lines < 10
|
|
529
|
+
score
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def detect_components_in_template(content)
|
|
533
|
+
used = []
|
|
534
|
+
used << :button if content.match?(/bg-\w+-[5-9]00.*text-white|btn-|<button/i)
|
|
535
|
+
used << :card if content.match?(/shadow.*rounded|card/i)
|
|
536
|
+
used << :input if content.match?(/<input|text_field|email_field|password_field/i)
|
|
537
|
+
used << :form if content.match?(/form_with|form_for|<form/i)
|
|
538
|
+
used << :link if content.match?(/link_to|<a\b/i)
|
|
539
|
+
used << :badge if content.match?(/rounded-full.*text-(?:xs|sm)|badge/i)
|
|
540
|
+
used << :grid if content.match?(/grid-cols-|grid\s+grid-/)
|
|
541
|
+
used
|
|
542
|
+
end
|
|
543
|
+
|
|
335
544
|
EXCLUDED_METHODS = %w[
|
|
336
545
|
each map select reject first last size count any? empty? present? blank?
|
|
337
546
|
new build create find where order limit nil? join class html_safe
|
|
@@ -6,6 +6,7 @@ module RailsAiContext
|
|
|
6
6
|
# These provide quick-reference lists without bloating CLAUDE.md.
|
|
7
7
|
class ClaudeRulesSerializer
|
|
8
8
|
include StackOverviewHelper
|
|
9
|
+
include DesignSystemHelper
|
|
9
10
|
|
|
10
11
|
attr_reader :context
|
|
11
12
|
|
|
@@ -250,31 +251,10 @@ module RailsAiContext
|
|
|
250
251
|
components = patterns[:components] || []
|
|
251
252
|
return nil if components.empty?
|
|
252
253
|
|
|
253
|
-
lines = [ "#
|
|
254
|
+
lines = [ "# Design System", "" ]
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
parts << "Primary: #{scheme[:primary]}" if scheme[:primary]
|
|
258
|
-
parts << "Text: #{scheme[:text]}" if scheme[:text]
|
|
259
|
-
lines << parts.join(" | ") if parts.any?
|
|
260
|
-
|
|
261
|
-
radius = patterns[:radius] || {}
|
|
262
|
-
if radius.any?
|
|
263
|
-
# Group by radius value to avoid repetition
|
|
264
|
-
by_radius = radius.group_by { |_, r| r }.map { |r, types| "#{r} (#{types.map(&:first).join(', ')})" }
|
|
265
|
-
lines << "Radius: #{by_radius.join(', ')}"
|
|
266
|
-
end
|
|
267
|
-
lines << ""
|
|
268
|
-
|
|
269
|
-
lines << "## Components"
|
|
270
|
-
components.first(20).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
|
|
271
|
-
|
|
272
|
-
fl = patterns[:form_layout] || {}
|
|
273
|
-
if fl.any?
|
|
274
|
-
lines << "" << "## Form layout"
|
|
275
|
-
lines << "- Spacing: #{fl[:spacing]}" if fl[:spacing]
|
|
276
|
-
lines << "- Grid: #{fl[:grid]}" if fl[:grid]
|
|
277
|
-
end
|
|
256
|
+
# Full design system with canonical examples
|
|
257
|
+
lines.concat(render_design_system_full(context))
|
|
278
258
|
|
|
279
259
|
# Shared partials — so agents reuse them instead of recreating
|
|
280
260
|
begin
|
|
@@ -346,7 +326,7 @@ module RailsAiContext
|
|
|
346
326
|
"- app/javascript/controllers/index.js — Stimulus auto-registers controllers. No need to read.",
|
|
347
327
|
"- Test files — use `rails_get_test_info(detail:\"full\")` for patterns.",
|
|
348
328
|
"",
|
|
349
|
-
"## Tools (
|
|
329
|
+
"## Tools (15)",
|
|
350
330
|
"",
|
|
351
331
|
"**rails_get_schema** — database tables, columns, indexes, foreign keys",
|
|
352
332
|
"- `rails_get_schema(detail:\"summary\")` — all tables with column counts",
|
|
@@ -387,6 +367,10 @@ module RailsAiContext
|
|
|
387
367
|
"**rails_analyze_feature** — combined schema + models + controllers + routes for a feature area",
|
|
388
368
|
"- `rails_analyze_feature(feature:\"authentication\")` — one call gets everything related to a feature",
|
|
389
369
|
"",
|
|
370
|
+
"**rails_get_design_system** — color palette, component patterns, canonical page examples",
|
|
371
|
+
"- `rails_get_design_system(detail:\"standard\")` — colors + components + real HTML examples + design rules",
|
|
372
|
+
"- `rails_get_design_system(detail:\"full\")` — + typography, responsive, dark mode, layout, spacing",
|
|
373
|
+
"",
|
|
390
374
|
"**rails_get_config** — cache store, session, timezone, middleware, initializers",
|
|
391
375
|
"**rails_get_gems** — notable gems categorized by function",
|
|
392
376
|
"**rails_get_conventions** — architecture patterns, directory structure",
|
|
@@ -8,6 +8,7 @@ module RailsAiContext
|
|
|
8
8
|
class ClaudeSerializer
|
|
9
9
|
include TestCommandDetection
|
|
10
10
|
include StackOverviewHelper
|
|
11
|
+
include DesignSystemHelper
|
|
11
12
|
|
|
12
13
|
attr_reader :context
|
|
13
14
|
|
|
@@ -192,40 +193,12 @@ module RailsAiContext
|
|
|
192
193
|
end
|
|
193
194
|
|
|
194
195
|
def render_ui_patterns
|
|
195
|
-
|
|
196
|
-
return [] unless vt.is_a?(Hash) && !vt[:error]
|
|
197
|
-
patterns = vt[:ui_patterns] || {}
|
|
198
|
-
components = patterns[:components] || []
|
|
199
|
-
return [] if components.empty?
|
|
200
|
-
|
|
201
|
-
lines = [ "## UI Patterns" ]
|
|
202
|
-
|
|
203
|
-
# Color scheme summary
|
|
204
|
-
scheme = patterns[:color_scheme] || {}
|
|
205
|
-
if scheme.any?
|
|
206
|
-
parts = []
|
|
207
|
-
parts << "Primary: #{scheme[:primary]}" if scheme[:primary]
|
|
208
|
-
parts << "Text: #{scheme[:text]}" if scheme[:text]
|
|
209
|
-
lines << parts.join(" | ") if parts.any?
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
lines << ""
|
|
213
|
-
components.first(15).each do |comp|
|
|
214
|
-
lines << "- #{comp[:label]}: `#{comp[:classes]}`"
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Form layout
|
|
218
|
-
fl = patterns[:form_layout] || {}
|
|
219
|
-
if fl.any?
|
|
220
|
-
lines << "" << "Form: #{fl.values.join(', ')}"
|
|
221
|
-
end
|
|
222
|
-
lines << ""
|
|
223
|
-
lines
|
|
196
|
+
render_design_system(context, max_lines: 30)
|
|
224
197
|
end
|
|
225
198
|
|
|
226
199
|
def render_mcp_guide # rubocop:disable Metrics/MethodLength
|
|
227
200
|
[
|
|
228
|
-
"## MCP Tools (
|
|
201
|
+
"## MCP Tools (15) — ALWAYS Use These First",
|
|
229
202
|
"",
|
|
230
203
|
"Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
|
|
231
204
|
"MCP tools return line numbers. Start with `detail:\"summary\"`.",
|
|
@@ -238,6 +211,7 @@ module RailsAiContext
|
|
|
238
211
|
"- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
|
|
239
212
|
"- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — tests",
|
|
240
213
|
"- `rails_analyze_feature(feature:\"auth\")` — schema + models + controllers + routes for a feature",
|
|
214
|
+
"- `rails_get_design_system` — color palette, component patterns, canonical page examples",
|
|
241
215
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
|
|
242
216
|
"- `rails_validate(files:[\"path/to/file.rb\"])` — validate Ruby, ERB, JS syntax in one call",
|
|
243
217
|
""
|
|
@@ -6,6 +6,7 @@ module RailsAiContext
|
|
|
6
6
|
# for GitHub Copilot path-specific instructions.
|
|
7
7
|
class CopilotInstructionsSerializer
|
|
8
8
|
include StackOverviewHelper
|
|
9
|
+
include DesignSystemHelper
|
|
9
10
|
|
|
10
11
|
attr_reader :context
|
|
11
12
|
|
|
@@ -193,37 +194,10 @@ module RailsAiContext
|
|
|
193
194
|
"---",
|
|
194
195
|
"applyTo: \"app/views/**/*.erb\"",
|
|
195
196
|
"---",
|
|
196
|
-
"",
|
|
197
|
-
"# UI Patterns",
|
|
198
197
|
""
|
|
199
198
|
]
|
|
200
|
-
components.first(15).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
|
|
201
|
-
|
|
202
|
-
# Shared partials
|
|
203
|
-
begin
|
|
204
|
-
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
205
|
-
shared_dir = File.join(root, "app", "views", "shared")
|
|
206
|
-
if Dir.exist?(shared_dir)
|
|
207
|
-
partials = Dir.glob(File.join(shared_dir, "_*.html.erb")).map { |f| File.basename(f) }.sort
|
|
208
|
-
if partials.any?
|
|
209
|
-
lines << "" << "## Shared partials"
|
|
210
|
-
partials.each { |p| lines << "- #{p}" }
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
rescue; end
|
|
214
199
|
|
|
215
|
-
|
|
216
|
-
begin
|
|
217
|
-
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
218
|
-
helper_file = File.join(root, "app", "helpers", "application_helper.rb")
|
|
219
|
-
if File.exist?(helper_file)
|
|
220
|
-
helper_methods = File.read(helper_file).scan(/def\s+(\w+)/).flatten
|
|
221
|
-
if helper_methods.any?
|
|
222
|
-
lines << "" << "## Helpers (ApplicationHelper)"
|
|
223
|
-
helper_methods.each { |m| lines << "- #{m}" }
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
rescue; end
|
|
200
|
+
lines.concat(render_design_system_full(context))
|
|
227
201
|
|
|
228
202
|
# Stimulus controllers
|
|
229
203
|
stim = context[:stimulus]
|
|
@@ -245,7 +219,7 @@ module RailsAiContext
|
|
|
245
219
|
"applyTo: \"**/*\"",
|
|
246
220
|
"---",
|
|
247
221
|
"",
|
|
248
|
-
"# Rails MCP Tools (
|
|
222
|
+
"# Rails MCP Tools (15) — Use These First",
|
|
249
223
|
"",
|
|
250
224
|
"Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
|
|
251
225
|
"MCP tools return line numbers for surgical edits.",
|
|
@@ -259,6 +233,7 @@ module RailsAiContext
|
|
|
259
233
|
"- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
|
|
260
234
|
"- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — existing tests",
|
|
261
235
|
"- `rails_analyze_feature(feature:\"auth\")` — schema + models + controllers + routes for a feature",
|
|
236
|
+
"- `rails_get_design_system` — color palette, components, canonical page examples",
|
|
262
237
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
|
|
263
238
|
"- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit context with line numbers",
|
|
264
239
|
"- `rails_validate(files:[\"path\"])` — validate Ruby, ERB, JS syntax in one call",
|
|
@@ -8,6 +8,7 @@ module RailsAiContext
|
|
|
8
8
|
class CopilotSerializer
|
|
9
9
|
include TestCommandDetection
|
|
10
10
|
include StackOverviewHelper
|
|
11
|
+
include DesignSystemHelper
|
|
11
12
|
|
|
12
13
|
attr_reader :context
|
|
13
14
|
|
|
@@ -87,16 +88,8 @@ module RailsAiContext
|
|
|
87
88
|
end
|
|
88
89
|
end
|
|
89
90
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
if vt.is_a?(Hash) && !vt[:error]
|
|
93
|
-
components = vt.dig(:ui_patterns, :components) || []
|
|
94
|
-
if components.any?
|
|
95
|
-
lines << "## UI Patterns"
|
|
96
|
-
components.first(15).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
|
|
97
|
-
lines << ""
|
|
98
|
-
end
|
|
99
|
-
end
|
|
91
|
+
# Design System
|
|
92
|
+
lines.concat(render_design_system(context, max_lines: 35))
|
|
100
93
|
|
|
101
94
|
# MCP tools
|
|
102
95
|
lines << "## MCP Tool Reference"
|