rails-ai-context 1.2.1 → 1.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 +22 -0
- data/ROADMAP.md +148 -0
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +28 -6
- data/lib/rails_ai_context/introspectors/model_introspector.rb +2 -1
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +128 -3
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -24
- data/lib/rails_ai_context/serializers/design_system_helper.rb +47 -0
- data/lib/rails_ai_context/tools/analyze_feature.rb +336 -68
- data/lib/rails_ai_context/tools/get_design_system.rb +21 -3
- data/lib/rails_ai_context/tools/validate.rb +7 -4
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aeeb02e9069ff67b12236ae2f99f169df5b5dd48d42af344ad0734ff1a499307
|
|
4
|
+
data.tar.gz: 0d5f18c57c512747bc06fc063c9e58c95073f288df15f3d74bd0d951815fabe8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc0c90e9d22ff8348ce6eefc770ca36eac77bda624795be2813972de0f56b565180bfc974042cc7fa949718a8362c760d3db9123dc8aab294403ca9498efd8f3
|
|
7
|
+
data.tar.gz: f33d929cf59fdd217d4905a49b5043b689cb7828588ff738d9973d2e97c42f78228327d3574d276180e04663cac3f3470c7b84bc118022d9dc8890a47ff117ed
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.3.0] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Full-stack `analyze_feature` tool** — now discovers services (AF1), jobs with queue/retry config (AF2), views with partial/Stimulus refs (AF3), Stimulus controllers with targets/values/actions (AF4), test files with counts (AF5), related models via associations (AF6), concern tracing (AF12), callback chains (AF13), channels (AF10), mailers (AF11), and environment variable dependencies (AF9). One call returns the complete feature picture.
|
|
13
|
+
- **Modal pattern extraction** (DS1) — detects overlay (`fixed inset-0 bg-black/50`) and modal card patterns
|
|
14
|
+
- **List item pattern extraction** (DS5) — detects repeating card/item patterns from views
|
|
15
|
+
- **Shared partials with descriptions** (DS7) — scans `app/views/shared/` and infers purpose (flash, navbar, status badge, loading, modal, etc.)
|
|
16
|
+
- **"When to use what" decision guide** (DS8) — explicit rules: primary button for CTAs, danger for destructive, when to use shared partials
|
|
17
|
+
- **Bootstrap component extraction** (DS13-DS15) — detects `btn-primary`, `card`, `modal`, `form-control`, `badge`, `alert`, `nav` patterns from Bootstrap apps
|
|
18
|
+
- **Tailwind `@apply` directive parsing** (DS16) — extracts named component classes from CSS `@apply` rules
|
|
19
|
+
- **DaisyUI/Flowbite/Headless UI detection** (DS17) — reports Tailwind plugin libraries from package.json
|
|
20
|
+
- **Animation/transition inventory** (DS19) — extracts `transition-*`, `duration-*`, `animate-*`, `ease-*` patterns
|
|
21
|
+
- **Smarter JSONB strong params check** (V1) — only skips params matching JSON column names, validates the rest
|
|
22
|
+
- **Route-action fix suggestions** (V2) — suggests "add `def action; end`" when route exists but action is missing
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`self` filtered from class methods** (B2/MD1) — no longer appears in model class method lists
|
|
27
|
+
- **Rules serializer methods cap raised to 20** (RS1) — uses introspector's pre-filtered methods directly instead of redundant re-filtering
|
|
28
|
+
- **oklch token noise filtered** (DS21) — complex color values (oklch, calc, var) hidden from summary, only shown in `detail:"full"`
|
|
29
|
+
|
|
8
30
|
## [1.2.1] - 2026-03-23
|
|
9
31
|
|
|
10
32
|
### Fixed
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# rails-ai-context — Bugs & Improvements Roadmap
|
|
2
|
+
|
|
3
|
+
> Comprehensive list from 6 testing sessions, ~500+ MCP calls, 522 specs, verified on DailyContentChef (9 tables, 6 models, 18 controllers, 15 Stimulus controllers).
|
|
4
|
+
> Current version: v1.2.1 (updated 2026-03-23)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Open Bugs
|
|
9
|
+
|
|
10
|
+
### MCP Tools
|
|
11
|
+
|
|
12
|
+
| # | Bug | Severity | Tool | Status |
|
|
13
|
+
|---|-----|----------|------|--------|
|
|
14
|
+
| B1 | `unless: :devise_controller?` not fully evaluated — OmniauthCallbacksController shows `authenticate_user!` | LOW | controllers | **FIXED v1.2.1** — evaluates condition at introspection time, removes filter for Devise controllers |
|
|
15
|
+
| B2 | `self` appears in class methods list — Plan shows `self` as a class method alongside `free`, `pro`, `business` | LOW | model_details | Open |
|
|
16
|
+
|
|
17
|
+
### Rules Serializer (generated CLAUDE.md / rules files)
|
|
18
|
+
|
|
19
|
+
| # | Bug | Severity | Status |
|
|
20
|
+
|---|-----|----------|--------|
|
|
21
|
+
| R1 | User methods list shows ~5 of 18 — missing concern + model-defined methods | MEDIUM | **FIXED v1.2.1** — source-defined methods prioritized, Devise methods filtered |
|
|
22
|
+
| R3 | `visuals_needed` shown as `string` not `string[]` in rules | MEDIUM | **FIXED v1.2.1** — array columns now render as `type[]` |
|
|
23
|
+
| R4 | payments missing `paymongo_checkout_id` and `paymongo_payment_id` columns in rules | MEDIUM | **FIXED v1.2.1** — external ID columns no longer hidden |
|
|
24
|
+
| R5 | users missing `paymongo_customer_id` column in rules | MEDIUM | **FIXED v1.2.1** — same fix as R4 |
|
|
25
|
+
| R6 | No column defaults shown in generated rules | MEDIUM | **FIXED v1.2.1** — defaults shown inline as `(=value)` |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Improvements: `rails_analyze_feature`
|
|
30
|
+
|
|
31
|
+
### Tier 1 — Core (makes the tool 10x more useful)
|
|
32
|
+
|
|
33
|
+
| # | Improvement | Description | Impact |
|
|
34
|
+
|---|-------------|-------------|--------|
|
|
35
|
+
| AF1 | **Services discovery** | Scan `app/services/` for classes matching the feature keyword. Show class name, line count, key method names. `feature:"cook"` → finds `ContentChefService`, `GeminiClient`, `OutputParser` | HIGH |
|
|
36
|
+
| AF2 | **Jobs discovery** | Scan `app/jobs/` for matching classes. Show queue name, retry config, what service it calls. `feature:"cook"` → finds `CookJob` (queue: default, retries: 3, calls ContentChefService) | HIGH |
|
|
37
|
+
| AF3 | **Views + partials discovery** | List matching views with line counts, partial renders, and Stimulus controller references. `feature:"cook"` → shows `cooks/show.html.erb (61 lines) renders: cooks/output, cooks/loading stimulus: cook-status, share` | HIGH |
|
|
38
|
+
| AF4 | **Stimulus controllers discovery** | Match Stimulus controllers by name. Show targets, values, actions. `feature:"cook"` → finds `cook_status` controller with `cookId` value and `checkTimeout` action | HIGH |
|
|
39
|
+
| AF5 | **Test files discovery** | List matching test files with test counts. `feature:"cook"` → shows `test/models/cook_test.rb (13 tests)`, `test/controllers/cooks_controller_test.rb (21 tests)` | HIGH |
|
|
40
|
+
|
|
41
|
+
### Tier 2 — Cross-cutting intelligence
|
|
42
|
+
|
|
43
|
+
| # | Improvement | Description | Impact |
|
|
44
|
+
|---|-------------|-------------|--------|
|
|
45
|
+
| AF6 | **Related models via associations** | Show models connected through `belongs_to`, `has_many`. `feature:"cook"` → "Related: User (owner), BrandProfile (optional), CookShare (shares)" | MEDIUM |
|
|
46
|
+
| AF7 | **Execution flow graph** | Trace the full request lifecycle: controller action → authorization check → model operation → job enqueue → service call → external API → broadcast. No other tool does this. | MEDIUM |
|
|
47
|
+
| AF8 | **Permission/authorization mapping** | Map which concern methods guard which actions. `can_cook?` guards `CooksController#create`, `can_use_bonus_modes?` guards `Bonus::BaseController` | MEDIUM |
|
|
48
|
+
| AF9 | **Environment dependencies** | Detect ENV vars referenced by the feature. `feature:"cook"` → requires `GEMINI_API_KEY`, Sidekiq running, Redis connected | MEDIUM |
|
|
49
|
+
| AF10 | **Channel/websocket discovery** | Find `turbo_stream_from` and Action Cable subscriptions. `feature:"cook"` → uses `turbo_stream_from "cook_#{id}"` for real-time output | MEDIUM |
|
|
50
|
+
|
|
51
|
+
### Tier 3 — Agent workflow optimization
|
|
52
|
+
|
|
53
|
+
| # | Improvement | Description | Impact |
|
|
54
|
+
|---|-------------|-------------|--------|
|
|
55
|
+
| AF11 | **Mailer/notification discovery** | Scan `app/mailers/` for matching classes and their delivery triggers | LOW |
|
|
56
|
+
| AF12 | **Concern tracing** | When a feature uses concerns, list which concerns and their methods. `feature:"User"` → PlanLimitable adds 12 methods | LOW |
|
|
57
|
+
| AF13 | **Callback chains** | Show before/after hooks that fire. `feature:"brand"` → `before_save :ensure_single_default` on BrandProfile | LOW |
|
|
58
|
+
| AF14 | **"How to extend" hints** | Based on existing patterns, suggest where to add a new action, new validation, new partial. "To add a new cook mode: add to MODES constant (line 7), add view in bonus/" | LOW |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Improvements: `rails_get_design_system`
|
|
63
|
+
|
|
64
|
+
### Tier 1 — Missing component patterns
|
|
65
|
+
|
|
66
|
+
| # | Improvement | Description | Impact |
|
|
67
|
+
|---|-------------|-------------|--------|
|
|
68
|
+
| DS1 | **Modal pattern** | Extract overlay + card pattern from `_share_modal.html.erb`. Show: `fixed inset-0 bg-black/50 z-40` overlay + `bg-white rounded-2xl shadow-lg max-w-md w-full p-6` card | HIGH |
|
|
69
|
+
| DS2 | **Badge/tag pattern** | Extract from mode badges: `text-xs font-medium px-2.5 py-1 rounded-full bg-{color}-100 text-{color}-700`. Show color variants (indigo, green, yellow, red) | HIGH |
|
|
70
|
+
| DS3 | **Status indicator pattern** | Extract from `_status_badge.html.erb`. Show as a reusable shared partial reference: `render "shared/status_badge", cook: cook` | HIGH |
|
|
71
|
+
| DS4 | **Flash/toast patterns** | Extract from `_flash.html.erb`. Show success (`bg-green-50 border-green-200 text-green-700`), error (`bg-red-50 border-red-200 text-red-700`), notice variants | HIGH |
|
|
72
|
+
| DS5 | **List item pattern** | Extract the repeating card-per-item layout from cook index: `bg-white rounded-xl p-5 shadow-sm border border-gray-100 flex items-center justify-between gap-4` | HIGH |
|
|
73
|
+
| DS6 | **Secondary button** | Extract: `bg-gray-100 text-gray-700 px-4 py-2 rounded-xl text-sm font-semibold hover:bg-gray-200 transition cursor-pointer`. Currently only primary + danger listed | MEDIUM |
|
|
74
|
+
| DS7 | **Shared partials section** | List all `app/views/shared/` partials with one-line descriptions. Agents should reuse these before creating new markup: `_flash.html.erb`, `_navbar.html.erb`, `_status_badge.html.erb`, `_upgrade_nudge.html.erb` | MEDIUM |
|
|
75
|
+
|
|
76
|
+
### Tier 2 — Decision guidance
|
|
77
|
+
|
|
78
|
+
| # | Improvement | Description | Impact |
|
|
79
|
+
|---|-------------|-------------|--------|
|
|
80
|
+
| DS8 | **"When to use what" decision guide** | Not just class strings but rules: "Page needs a form? → Copy Form Page example. Need confirmation? → `data: { turbo_confirm: 'message' }`. Showing status? → `render 'shared/status_badge'`" | HIGH |
|
|
81
|
+
| DS9 | **Loading/spinner pattern** | Extract from `_loading.html.erb`: `animate-spin` emoji + progress bar (`bg-orange-500 h-2 rounded-full animate-pulse`) | MEDIUM |
|
|
82
|
+
| DS10 | **Confirmation dialog convention** | Document the Turbo Confirm pattern: `data: { turbo_confirm: "Are you sure?" }` on `button_to` for destructive actions | MEDIUM |
|
|
83
|
+
| DS11 | **Form error pattern** | Show what validation errors look like: field highlighting, error message placement, `field_with_errors` wrapper behavior | MEDIUM |
|
|
84
|
+
| DS12 | **Spacing system rules** | Explain WHEN to use each spacing: `space-y-3` for list items, `space-y-4` for form fields, `space-y-6` for form sections, `gap-2` for button groups, `mb-6` for section separators | LOW |
|
|
85
|
+
|
|
86
|
+
### Tier 3 — Framework adaptability
|
|
87
|
+
|
|
88
|
+
| # | Improvement | Description | Impact |
|
|
89
|
+
|---|-------------|-------------|--------|
|
|
90
|
+
| DS13 | **Auto-detect CSS framework** | Detect Tailwind vs Bootstrap vs custom CSS/Sass. Adapt extraction strategy per framework. Currently hardcoded for Tailwind — broken for all other setups | HIGH |
|
|
91
|
+
| DS14 | **Bootstrap support** | Scan ERB for Bootstrap classes (`btn-primary`, `card`, `modal`, `form-control`). Parse `_variables.scss` for custom theme. Show Bootstrap component examples from actual views | HIGH (for Bootstrap apps) |
|
|
92
|
+
| DS15 | **Custom CSS/Sass support** | Parse `.scss/.css` files for class definitions. Group by file (buttons.scss → Button patterns). Detect BEM naming. Show CSS custom properties (`--color-primary`) | HIGH (for custom apps) |
|
|
93
|
+
| DS16 | **Parse Tailwind `@apply` directives** | If app has `@apply` rules in CSS, extract those as named component classes | MEDIUM |
|
|
94
|
+
| DS17 | **Detect DaisyUI / Flowbite / Headless UI** | If Tailwind plugin libraries are installed, include their component patterns alongside raw Tailwind | MEDIUM |
|
|
95
|
+
| DS18 | **Parse `tailwind.config.js` custom theme** | Extract custom colors, fonts, spacing from the config file. Show `primary: '#FF6B00'` if customized | MEDIUM |
|
|
96
|
+
| DS19 | **Animation/transition inventory** | List all `transition`, `animate-*`, `duration-*` patterns with usage context | LOW |
|
|
97
|
+
| DS20 | **Icon size conventions** | Document when to use which size: `w-3.5 h-3.5` (inline with text), `w-4 h-4` (buttons), `w-5 h-5` (standalone), `w-10 h-10` (feature icons) | LOW |
|
|
98
|
+
| DS21 | **Remove oklch noise from summary** | Token colors (oklch values) waste tokens in summary. Move to `detail:"full"` only. Summary should show Tailwind class names only | LOW |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Improvements: Rules Serializer
|
|
103
|
+
|
|
104
|
+
| # | Improvement | Description | Impact |
|
|
105
|
+
|---|-------------|-------------|--------|
|
|
106
|
+
| RS1 | **Include all concern methods** | User methods list should include all PlanLimitable methods (12+), not just 5 | HIGH — partially fixed in v1.2.1 (source methods prioritized), full concern method extraction still open |
|
|
107
|
+
| RS2 | **Detect array columns** | Show `visuals_needed:string[]` not `visuals_needed:string` | ~~MEDIUM~~ **FIXED v1.2.1** |
|
|
108
|
+
| RS3 | **Include all non-system columns** | payments should show `paymongo_checkout_id`, `paymongo_payment_id`. users should show `paymongo_customer_id` | ~~MEDIUM~~ **FIXED v1.2.1** |
|
|
109
|
+
| RS4 | **Show column defaults** | Inline defaults: `mode:string(default:"standard")`, `status:string(default:"pending")` | ~~MEDIUM~~ **FIXED v1.2.1** |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Improvements: `rails_validate`
|
|
114
|
+
|
|
115
|
+
| # | Improvement | Description | Impact |
|
|
116
|
+
|---|-------------|-------------|--------|
|
|
117
|
+
| V1 | **Smarter JSONB strong params skip** | Currently skips ALL params check for models with ANY JSONB column. Could be smarter: only skip params matching JSONB column names, check the rest | LOW |
|
|
118
|
+
| V2 | **Route-action check suggests fix** | When `show` action missing but route exists, suggest: "Add `def show; end` to BrandProfilesController or remove `show` from `resources :brand_profiles`" | LOW |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Improvements: `rails_get_model_details`
|
|
123
|
+
|
|
124
|
+
| # | Improvement | Description | Impact |
|
|
125
|
+
|---|-------------|-------------|--------|
|
|
126
|
+
| MD1 | **Filter `self` from class methods** | Plan shows `self` as a class method — should be filtered out | LOW |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Summary
|
|
131
|
+
|
|
132
|
+
| Category | Total | Fixed | Open | Tier 1 (HIGH) | Tier 2 (MEDIUM) | Tier 3 (LOW) |
|
|
133
|
+
|----------|-------|-------|------|--------------|-----------------|--------------|
|
|
134
|
+
| Open Bugs | 7 | 6 | 1 | 0 | 0 | 1 (B2) |
|
|
135
|
+
| analyze_feature | 14 | 0 | 14 | 5 | 5 | 4 |
|
|
136
|
+
| design_system | 21 | 0 | 21 | 9 | 6 | 6 |
|
|
137
|
+
| Rules serializer | 4 | 3 | 1 | 1 (RS1 partial) | 0 | 0 |
|
|
138
|
+
| validate | 2 | 0 | 2 | 0 | 0 | 2 |
|
|
139
|
+
| model_details | 1 | 0 | 1 | 0 | 0 | 1 |
|
|
140
|
+
| **Total** | **49** | **9** | **40** | **15** | **11** | **14** |
|
|
141
|
+
|
|
142
|
+
### Killer differentiators (no other tool does these)
|
|
143
|
+
|
|
144
|
+
1. **Execution flow graph** (AF7) — trace a full request from HTTP to database to broadcast in one call
|
|
145
|
+
2. **"When to use what" decision guide** (DS8) — not just patterns but rules for choosing the right one
|
|
146
|
+
3. **Auto-detect CSS framework** (DS13-DS15) — works for Tailwind, Bootstrap, custom CSS, any Rails app
|
|
147
|
+
4. **Services + Jobs + Views + Tests in feature analysis** (AF1-AF5) — full-stack feature discovery in one call
|
|
148
|
+
5. **Environment dependency detection** (AF9) — know what needs to be running before you touch a feature
|
|
@@ -171,6 +171,10 @@ module RailsAiContext
|
|
|
171
171
|
f[:if] = sc[:if] if sc[:if]
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
|
+
|
|
175
|
+
# Evaluate known runtime conditions to remove inapplicable filters
|
|
176
|
+
reflection_filters.reject! { |f| filter_excluded_by_condition?(ctrl, f) }
|
|
177
|
+
|
|
174
178
|
return reflection_filters
|
|
175
179
|
end
|
|
176
180
|
end
|
|
@@ -258,6 +262,29 @@ module RailsAiContext
|
|
|
258
262
|
nil
|
|
259
263
|
end
|
|
260
264
|
|
|
265
|
+
# Statically evaluate known runtime conditions to exclude inapplicable filters.
|
|
266
|
+
# e.g., `unless: :devise_controller?` on a Devise controller means the filter doesn't apply.
|
|
267
|
+
def filter_excluded_by_condition?(ctrl, filter)
|
|
268
|
+
# unless: :devise_controller? — filter does NOT apply to Devise controllers
|
|
269
|
+
if filter[:unless] == "devise_controller?"
|
|
270
|
+
return true if devise_controller?(ctrl)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# if: :devise_controller? — filter ONLY applies to Devise controllers
|
|
274
|
+
if filter[:if] == "devise_controller?"
|
|
275
|
+
return true unless devise_controller?(ctrl)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
false
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def devise_controller?(ctrl)
|
|
282
|
+
return false unless defined?(::DeviseController)
|
|
283
|
+
ctrl < ::DeviseController || ctrl.ancestors.any? { |a| a.name&.start_with?("Devise::") }
|
|
284
|
+
rescue
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
|
|
261
288
|
def extract_action_condition(condition)
|
|
262
289
|
return nil unless condition.is_a?(String) || condition.respond_to?(:to_s)
|
|
263
290
|
match = condition.to_s.match(/action_name\s*==\s*['"](\w+)['"]/)
|
|
@@ -32,6 +32,7 @@ module RailsAiContext
|
|
|
32
32
|
extract_css_custom_properties(root, tokens)
|
|
33
33
|
extract_webpacker_styles(root, tokens)
|
|
34
34
|
extract_component_css(root, tokens)
|
|
35
|
+
extract_apply_directives(root, tokens)
|
|
35
36
|
|
|
36
37
|
return { skipped: true, reason: "No design tokens found" } if tokens.empty?
|
|
37
38
|
|
|
@@ -48,20 +49,29 @@ module RailsAiContext
|
|
|
48
49
|
|
|
49
50
|
def detect_framework(root)
|
|
50
51
|
gemfile = File.join(root, "Gemfile")
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
package_json = File.join(root, "package.json")
|
|
53
|
+
gemfile_content = File.exist?(gemfile) ? (File.read(gemfile, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue "") : ""
|
|
54
|
+
pkg_content = File.exist?(package_json) ? (File.read(package_json, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue "") : ""
|
|
53
55
|
|
|
54
|
-
if
|
|
56
|
+
framework = if gemfile_content.include?("tailwindcss-rails")
|
|
55
57
|
"tailwind"
|
|
56
|
-
elsif
|
|
58
|
+
elsif gemfile_content.include?("bootstrap")
|
|
57
59
|
"bootstrap"
|
|
58
|
-
elsif
|
|
60
|
+
elsif gemfile_content.include?("dartsass-rails") || gemfile_content.include?("sassc-rails") || gemfile_content.include?("sass-rails")
|
|
59
61
|
"sass"
|
|
60
|
-
elsif
|
|
62
|
+
elsif gemfile_content.include?("cssbundling-rails")
|
|
61
63
|
"cssbundling"
|
|
62
64
|
else
|
|
63
65
|
"plain_css"
|
|
64
66
|
end
|
|
67
|
+
|
|
68
|
+
# DS17: Detect Tailwind plugin libraries
|
|
69
|
+
plugins = []
|
|
70
|
+
plugins << "daisyui" if pkg_content.include?("daisyui") || gemfile_content.include?("daisyui")
|
|
71
|
+
plugins << "flowbite" if pkg_content.include?("flowbite")
|
|
72
|
+
plugins << "headlessui" if pkg_content.include?("headlessui") || pkg_content.include?("@headlessui")
|
|
73
|
+
|
|
74
|
+
plugins.any? ? "#{framework}+#{plugins.join('+')}" : framework
|
|
65
75
|
rescue
|
|
66
76
|
"unknown"
|
|
67
77
|
end
|
|
@@ -238,6 +248,18 @@ module RailsAiContext
|
|
|
238
248
|
categories.reject { |_, v| v.empty? }
|
|
239
249
|
end
|
|
240
250
|
|
|
251
|
+
# DS16: Extract @apply directives as named component classes
|
|
252
|
+
def extract_apply_directives(root, tokens)
|
|
253
|
+
%w[app/assets/stylesheets app/assets/tailwind].each do |dir|
|
|
254
|
+
Dir.glob(File.join(root, dir, "**", "*.css")).each do |path|
|
|
255
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
256
|
+
content.scan(/\.([a-zA-Z][\w-]*)\s*\{[^}]*@apply\s+([^;]+);/m).each do |name, classes|
|
|
257
|
+
tokens["@apply-#{name}"] = classes.strip
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
241
263
|
# Helper: extract :root { --var: value } from CSS content
|
|
242
264
|
def extract_root_vars(content, tokens)
|
|
243
265
|
content.scan(/:root\s*(?:,\s*:host)?\s*\{([^}]+)\}/m).each do |match|
|
|
@@ -239,7 +239,8 @@ module RailsAiContext
|
|
|
239
239
|
all_methods = (model.methods - ActiveRecord::Base.methods - Object.methods)
|
|
240
240
|
.reject { |m|
|
|
241
241
|
ms = m.to_s
|
|
242
|
-
ms
|
|
242
|
+
ms == "self" ||
|
|
243
|
+
ms.start_with?("_", "autosave") ||
|
|
243
244
|
scope_names.include?(ms) ||
|
|
244
245
|
DEVISE_CLASS_METHOD_PATTERNS.include?(ms) ||
|
|
245
246
|
ms.end_with?("=") && ms.length > 20 # Devise setter-like methods
|
|
@@ -21,7 +21,8 @@ module RailsAiContext
|
|
|
21
21
|
templates: scan_templates(views_dir),
|
|
22
22
|
partials: scan_partials(views_dir),
|
|
23
23
|
ui_patterns: extract_ui_patterns(all_content).merge(
|
|
24
|
-
canonical_examples: extract_canonical_examples(views_dir)
|
|
24
|
+
canonical_examples: extract_canonical_examples(views_dir),
|
|
25
|
+
shared_partials: discover_shared_partials(views_dir)
|
|
25
26
|
)
|
|
26
27
|
}
|
|
27
28
|
rescue => e
|
|
@@ -86,7 +87,10 @@ module RailsAiContext
|
|
|
86
87
|
components = []
|
|
87
88
|
used = Set.new
|
|
88
89
|
|
|
89
|
-
#
|
|
90
|
+
# DS13-15: Framework-aware component extraction
|
|
91
|
+
extract_bootstrap_components(all_content, components, used) if all_content.match?(/btn-|form-control|card-body/)
|
|
92
|
+
|
|
93
|
+
# Extract element-aware patterns (Tailwind + generic)
|
|
90
94
|
extract_buttons(all_content, class_groups, components, used)
|
|
91
95
|
extract_cards(class_groups, components, used)
|
|
92
96
|
extract_inputs(all_content, class_groups, components, used)
|
|
@@ -98,6 +102,8 @@ module RailsAiContext
|
|
|
98
102
|
extract_headings(all_content, components)
|
|
99
103
|
extract_flashes(all_content, components)
|
|
100
104
|
extract_alerts(class_groups, components, used)
|
|
105
|
+
extract_modals(all_content, class_groups, components, used)
|
|
106
|
+
extract_list_items(class_groups, components, used)
|
|
101
107
|
|
|
102
108
|
# Design tokens
|
|
103
109
|
color_scheme = extract_color_scheme(all_content, class_groups)
|
|
@@ -114,7 +120,8 @@ module RailsAiContext
|
|
|
114
120
|
responsive: extract_responsive_patterns(all_content),
|
|
115
121
|
interactive_states: extract_interactive_states(all_content),
|
|
116
122
|
dark_mode: extract_dark_mode_patterns(all_content),
|
|
117
|
-
icons: extract_icon_system(all_content)
|
|
123
|
+
icons: extract_icon_system(all_content),
|
|
124
|
+
animations: extract_animations(all_content)
|
|
118
125
|
}
|
|
119
126
|
end
|
|
120
127
|
|
|
@@ -302,6 +309,71 @@ module RailsAiContext
|
|
|
302
309
|
components << { type: :alert, label: "Alert", classes: best[0] }
|
|
303
310
|
end
|
|
304
311
|
|
|
312
|
+
# DS13-15: Bootstrap component extraction
|
|
313
|
+
def extract_bootstrap_components(content, components, used)
|
|
314
|
+
bootstrap_patterns = {
|
|
315
|
+
"btn-primary" => { type: :button, label: "Button (primary)" },
|
|
316
|
+
"btn-secondary" => { type: :button, label: "Button (secondary)" },
|
|
317
|
+
"btn-danger" => { type: :button, label: "Button (danger)" },
|
|
318
|
+
"btn-outline-primary" => { type: :button, label: "Button (outline)" },
|
|
319
|
+
"card" => { type: :card, label: "Card" },
|
|
320
|
+
"modal" => { type: :modal_card, label: "Modal" },
|
|
321
|
+
"form-control" => { type: :input, label: "Input" },
|
|
322
|
+
"form-select" => { type: :select, label: "Select" },
|
|
323
|
+
"badge" => { type: :badge, label: "Badge" },
|
|
324
|
+
"alert" => { type: :alert, label: "Alert" },
|
|
325
|
+
"nav" => { type: :nav, label: "Navigation" }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
bootstrap_patterns.each do |pattern, meta|
|
|
329
|
+
matches = content.scan(/class=["'][^"']*\b#{pattern}\b[^"']*["']/).map do |m|
|
|
330
|
+
m.gsub(/class=["']|["']/, "").strip
|
|
331
|
+
end
|
|
332
|
+
next if matches.empty?
|
|
333
|
+
|
|
334
|
+
best = matches.tally.max_by { |_, c| c }
|
|
335
|
+
unless used.include?(best[0])
|
|
336
|
+
components << meta.merge(classes: best[0])
|
|
337
|
+
used << best[0]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# DS1: Modal/overlay patterns
|
|
343
|
+
def extract_modals(content, groups, components, used)
|
|
344
|
+
# Detect overlay pattern (fixed inset-0 bg-black/50 or similar)
|
|
345
|
+
overlay = groups.select { |c, _| c.match?(/fixed.*inset-0|fixed.*z-\d+.*bg-/) && !used.include?(c) }
|
|
346
|
+
if overlay.any?
|
|
347
|
+
best = overlay.max_by { |_, count| count }
|
|
348
|
+
components << { type: :modal_overlay, label: "Modal overlay", classes: best[0] }
|
|
349
|
+
used << best[0]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Detect modal card (usually the child of overlay)
|
|
353
|
+
modal_card = groups.select do |c, _|
|
|
354
|
+
c.match?(/bg-white.*rounded.*shadow.*max-w-|modal/) && c.match?(/p-\d/) && !used.include?(c)
|
|
355
|
+
end
|
|
356
|
+
if modal_card.any?
|
|
357
|
+
best = modal_card.max_by { |_, count| count }
|
|
358
|
+
components << { type: :modal_card, label: "Modal card", classes: best[0] }
|
|
359
|
+
used << best[0]
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# DS5: List item / repeating card pattern
|
|
364
|
+
def extract_list_items(groups, components, used)
|
|
365
|
+
candidates = groups.select do |c, count|
|
|
366
|
+
count >= 3 && # appears 3+ times (repeating pattern)
|
|
367
|
+
c.match?(/(?:flex|grid).*(?:items-|justify-|gap-)/) &&
|
|
368
|
+
c.match?(/(?:bg-white|border|shadow|rounded)/) &&
|
|
369
|
+
!used.include?(c)
|
|
370
|
+
end
|
|
371
|
+
return if candidates.empty?
|
|
372
|
+
best = candidates.max_by { |_, count| count }
|
|
373
|
+
components << { type: :list_item, label: "List item", classes: best[0] }
|
|
374
|
+
used << best[0]
|
|
375
|
+
end
|
|
376
|
+
|
|
305
377
|
def extract_color_scheme(content, groups)
|
|
306
378
|
# Find primary color from button backgrounds
|
|
307
379
|
primary_colors = Hash.new(0)
|
|
@@ -452,6 +524,29 @@ module RailsAiContext
|
|
|
452
524
|
icons.empty? ? nil : icons
|
|
453
525
|
end
|
|
454
526
|
|
|
527
|
+
# DS19: Animation and transition patterns
|
|
528
|
+
def extract_animations(content)
|
|
529
|
+
animations = {}
|
|
530
|
+
|
|
531
|
+
# Transition classes
|
|
532
|
+
transitions = content.scan(/transition(?:-\w+)*/).tally
|
|
533
|
+
animations[:transitions] = transitions.sort_by { |_, c| -c }.first(5).to_h unless transitions.empty?
|
|
534
|
+
|
|
535
|
+
# Duration classes
|
|
536
|
+
durations = content.scan(/duration-\d+/).tally
|
|
537
|
+
animations[:durations] = durations.sort_by { |_, c| -c }.first(3).to_h unless durations.empty?
|
|
538
|
+
|
|
539
|
+
# Animate classes
|
|
540
|
+
animates = content.scan(/animate-\w+/).tally
|
|
541
|
+
animations[:animates] = animates.sort_by { |_, c| -c }.first(5).to_h unless animates.empty?
|
|
542
|
+
|
|
543
|
+
# Ease classes
|
|
544
|
+
eases = content.scan(/ease-\w+/).tally
|
|
545
|
+
animations[:easing] = eases.sort_by { |_, c| -c }.first(3).to_h unless eases.empty?
|
|
546
|
+
|
|
547
|
+
animations.empty? ? nil : animations
|
|
548
|
+
end
|
|
549
|
+
|
|
455
550
|
# Analyzes individual templates to find canonical examples of common page types.
|
|
456
551
|
# Returns up to 5 representative ERB snippets that AI can copy.
|
|
457
552
|
def extract_canonical_examples(views_dir) # rubocop:disable Metrics
|
|
@@ -541,6 +636,36 @@ module RailsAiContext
|
|
|
541
636
|
used
|
|
542
637
|
end
|
|
543
638
|
|
|
639
|
+
# DS7: Shared partials with one-line descriptions
|
|
640
|
+
def discover_shared_partials(views_dir)
|
|
641
|
+
shared_dir = File.join(views_dir, "shared")
|
|
642
|
+
return [] unless Dir.exist?(shared_dir)
|
|
643
|
+
|
|
644
|
+
Dir.glob(File.join(shared_dir, "_*.{erb,haml,slim}")).sort.map do |path|
|
|
645
|
+
name = File.basename(path)
|
|
646
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue ""
|
|
647
|
+
description = infer_partial_description(name, content)
|
|
648
|
+
{ name: name, lines: content.lines.size, description: description }
|
|
649
|
+
end
|
|
650
|
+
rescue
|
|
651
|
+
[]
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def infer_partial_description(name, content)
|
|
655
|
+
# Infer from content patterns
|
|
656
|
+
return "Flash/notification messages" if name.include?("flash") || name.include?("notification")
|
|
657
|
+
return "Navigation bar" if name.include?("nav") || name.include?("header")
|
|
658
|
+
return "Footer" if name.include?("footer")
|
|
659
|
+
return "Status badge/indicator" if name.include?("status") || name.include?("badge")
|
|
660
|
+
return "Loading/spinner" if name.include?("loading") || name.include?("spinner") || content.include?("animate-spin")
|
|
661
|
+
return "Modal dialog" if name.include?("modal") || name.include?("dialog")
|
|
662
|
+
return "Form component" if name.include?("form") || content.match?(/form_with|form_for/)
|
|
663
|
+
return "Upgrade prompt" if name.include?("upgrade") || name.include?("nudge")
|
|
664
|
+
return "Share dialog" if name.include?("share")
|
|
665
|
+
return "Error display" if name.include?("error")
|
|
666
|
+
"Shared partial (#{content.lines.size} lines)"
|
|
667
|
+
end
|
|
668
|
+
|
|
544
669
|
EXCLUDED_METHODS = %w[
|
|
545
670
|
each map select reject first last size count any? empty? present? blank?
|
|
546
671
|
new build create find where order limit nil? join class html_safe
|
|
@@ -218,30 +218,8 @@ module RailsAiContext
|
|
|
218
218
|
scopes = data[:scopes] || []
|
|
219
219
|
lines << " scopes: #{scopes.join(', ')}" if scopes.any?
|
|
220
220
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
_ids _ids= _before_last_save _before_type_cast _came_from_user?
|
|
224
|
-
_for_database _in_database _was]
|
|
225
|
-
# Filter out association getters (cooks, user, plan, etc.)
|
|
226
|
-
assoc_names = (data[:associations] || []).flat_map { |a| [ a[:name].to_s, "#{a[:name]}=" ] }
|
|
227
|
-
# Filter out Devise and other framework-generated methods
|
|
228
|
-
devise_methods = %w[active_for_authentication? after_database_authentication after_remembered
|
|
229
|
-
authenticatable_salt inactive_message confirmation_required?
|
|
230
|
-
send_confirmation_instructions password_required? email_required?
|
|
231
|
-
will_save_change_to_email? clean_up_passwords current_password
|
|
232
|
-
destroy_with_password devise_mailer devise_modules devise_scope
|
|
233
|
-
send_devise_notification valid_password? update_with_password
|
|
234
|
-
send_reset_password_instructions apply_to_attribute_or_variable
|
|
235
|
-
allowed_gemini_models remember_me! forget_me!
|
|
236
|
-
skip_confirmation! skip_reconfirmation!]
|
|
237
|
-
devise_patterns = %w[devise_ _password _authenticatable _confirmation _recoverable]
|
|
238
|
-
methods = (data[:instance_methods] || []).reject { |m|
|
|
239
|
-
generated_patterns.any? { |p| m.include?(p) } ||
|
|
240
|
-
m.end_with?("=") ||
|
|
241
|
-
assoc_names.include?(m) ||
|
|
242
|
-
devise_methods.include?(m) ||
|
|
243
|
-
devise_patterns.any? { |p| m.include?(p) }
|
|
244
|
-
}.first(10)
|
|
221
|
+
# Instance methods — introspector already prioritizes source-defined and filters Devise
|
|
222
|
+
methods = (data[:instance_methods] || []).reject { |m| m.end_with?("=") }.first(20)
|
|
245
223
|
lines << " methods: #{methods.join(', ')}" if methods.any?
|
|
246
224
|
|
|
247
225
|
# Include constants (e.g. STATUSES, MODES) so agents know valid values
|
|
@@ -32,6 +32,7 @@ module RailsAiContext
|
|
|
32
32
|
lines.concat(render_spacing_summary(patterns))
|
|
33
33
|
lines.concat(render_interaction_summary(patterns))
|
|
34
34
|
lines.concat(render_dark_mode_summary(patterns))
|
|
35
|
+
lines.concat(render_decision_guide(patterns))
|
|
35
36
|
lines.concat(render_design_rules(patterns))
|
|
36
37
|
|
|
37
38
|
lines.first(max_lines)
|
|
@@ -83,6 +84,17 @@ module RailsAiContext
|
|
|
83
84
|
lines << ""
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
# Shared partials with descriptions
|
|
88
|
+
shared = patterns[:shared_partials] || []
|
|
89
|
+
if shared.any?
|
|
90
|
+
lines << "## Shared Partials — Reuse Before Creating New Markup"
|
|
91
|
+
lines << ""
|
|
92
|
+
shared.each do |p|
|
|
93
|
+
lines << "- `#{p[:name]}` — #{p[:description]}"
|
|
94
|
+
end
|
|
95
|
+
lines << ""
|
|
96
|
+
end
|
|
97
|
+
|
|
86
98
|
lines
|
|
87
99
|
end
|
|
88
100
|
|
|
@@ -201,6 +213,41 @@ module RailsAiContext
|
|
|
201
213
|
lines
|
|
202
214
|
end
|
|
203
215
|
|
|
216
|
+
# DS8: Decision guide — when to use what
|
|
217
|
+
def render_decision_guide(patterns)
|
|
218
|
+
components = patterns[:components] || []
|
|
219
|
+
return [] if components.size < 3
|
|
220
|
+
|
|
221
|
+
lines = [ "### When to Use What" ]
|
|
222
|
+
|
|
223
|
+
# Button decisions
|
|
224
|
+
has_primary = components.any? { |c| c[:label]&.include?("primary") }
|
|
225
|
+
has_danger = components.any? { |c| c[:label]&.include?("danger") }
|
|
226
|
+
has_secondary = components.any? { |c| c[:label]&.include?("secondary") }
|
|
227
|
+
if has_primary || has_danger
|
|
228
|
+
lines << "- **Primary action** (Save, Submit, Continue) → Primary button"
|
|
229
|
+
lines << "- **Secondary action** (Cancel, Back, Skip) → Secondary button" if has_secondary
|
|
230
|
+
lines << "- **Destructive action** (Delete, Remove) → Danger button" if has_danger
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Turbo confirm
|
|
234
|
+
lines << "- **Confirmation needed** → `data: { turbo_confirm: \"Are you sure?\" }` on `button_to`"
|
|
235
|
+
|
|
236
|
+
# Shared partials usage
|
|
237
|
+
shared = patterns[:shared_partials] || []
|
|
238
|
+
shared.each do |p|
|
|
239
|
+
case p[:name]
|
|
240
|
+
when /flash|notification/ then lines << "- **Show feedback** → `render \"shared/#{p[:name].sub(/\A_/, '').sub(/\..*/, '')}\"` "
|
|
241
|
+
when /status|badge/ then lines << "- **Show status** → `render \"shared/#{p[:name].sub(/\A_/, '').sub(/\..*/, '')}\"` "
|
|
242
|
+
when /modal|dialog/ then lines << "- **Need overlay/dialog** → `render \"shared/#{p[:name].sub(/\A_/, '').sub(/\..*/, '')}\"` "
|
|
243
|
+
when /loading|spinner/ then lines << "- **Show loading** → `render \"shared/#{p[:name].sub(/\A_/, '').sub(/\..*/, '')}\"` "
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
lines << ""
|
|
248
|
+
lines
|
|
249
|
+
end
|
|
250
|
+
|
|
204
251
|
def render_design_rules(patterns)
|
|
205
252
|
lines = [ "### Design Rules" ]
|
|
206
253
|
|
|
@@ -4,7 +4,8 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class AnalyzeFeature < BaseTool
|
|
6
6
|
tool_name "rails_analyze_feature"
|
|
7
|
-
description "
|
|
7
|
+
description "Full-stack feature analysis: models, controllers, routes, services, jobs, views, " \
|
|
8
|
+
"Stimulus controllers, tests, related models, callbacks, concerns, and environment dependencies. " \
|
|
8
9
|
"Use when: exploring an unfamiliar feature, onboarding to a codebase area, or tracing a feature across layers. " \
|
|
9
10
|
"Pass feature:\"authentication\" or feature:\"User\" for broad cross-cutting discovery."
|
|
10
11
|
|
|
@@ -12,7 +13,7 @@ module RailsAiContext
|
|
|
12
13
|
properties: {
|
|
13
14
|
feature: {
|
|
14
15
|
type: "string",
|
|
15
|
-
description: "Feature keyword to search for (e.g. 'authentication', 'User', 'payments', 'orders'). Case-insensitive partial match across
|
|
16
|
+
description: "Feature keyword to search for (e.g. 'authentication', 'User', 'payments', 'orders'). Case-insensitive partial match across all layers."
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
required: [ "feature" ]
|
|
@@ -20,95 +21,362 @@ module RailsAiContext
|
|
|
20
21
|
|
|
21
22
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
22
23
|
|
|
23
|
-
def self.call(feature:, server_context: nil)
|
|
24
|
+
def self.call(feature:, server_context: nil) # rubocop:disable Metrics
|
|
24
25
|
ctx = cached_context
|
|
25
26
|
pattern = feature.downcase
|
|
27
|
+
root = rails_app.root.to_s
|
|
26
28
|
lines = [ "# Feature Analysis: #{feature}", "" ]
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
matched_models = discover_models(ctx, pattern, lines)
|
|
31
|
+
discover_controllers(ctx, pattern, lines)
|
|
32
|
+
discover_routes(ctx, pattern, lines)
|
|
33
|
+
discover_services(root, pattern, lines)
|
|
34
|
+
discover_jobs(root, pattern, lines)
|
|
35
|
+
discover_views(ctx, root, pattern, lines)
|
|
36
|
+
discover_stimulus(ctx, pattern, lines)
|
|
37
|
+
discover_tests(root, pattern, lines)
|
|
38
|
+
discover_related_models(ctx, matched_models, lines)
|
|
39
|
+
discover_concerns(ctx, matched_models, lines)
|
|
40
|
+
discover_callbacks(ctx, matched_models, lines)
|
|
41
|
+
discover_channels(root, pattern, lines)
|
|
42
|
+
discover_mailers(root, pattern, lines)
|
|
43
|
+
discover_env_dependencies(root, pattern, matched_models, lines)
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
text_response(lines.join("\n"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# --- AF: Models ---
|
|
52
|
+
def discover_models(ctx, pattern, lines)
|
|
53
|
+
models = ctx[:models] || {}
|
|
54
|
+
matched = models.select do |name, data|
|
|
55
|
+
next false if data[:error]
|
|
56
|
+
name.downcase.include?(pattern) ||
|
|
57
|
+
data[:table_name]&.downcase&.include?(pattern) ||
|
|
58
|
+
name.underscore.include?(pattern)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if matched.any?
|
|
62
|
+
lines << "## Models (#{matched.size})"
|
|
63
|
+
matched.sort.each do |name, data|
|
|
64
|
+
lines << "" << "### #{name}"
|
|
65
|
+
lines << "**Table:** `#{data[:table_name]}`" if data[:table_name]
|
|
66
|
+
|
|
67
|
+
table_name = data[:table_name]
|
|
68
|
+
if table_name && (tables = ctx.dig(:schema, :tables))
|
|
69
|
+
table_data = tables[table_name]
|
|
70
|
+
if table_data&.dig(:columns)&.any?
|
|
71
|
+
cols = table_data[:columns].reject { |c| %w[id created_at updated_at].include?(c[:name]) }
|
|
72
|
+
col_strs = cols.map { |c| col_type = c[:array] ? "#{c[:type]}[]" : c[:type]; "#{c[:name]}:#{col_type}" }
|
|
73
|
+
lines << "**Columns:** #{col_strs.join(', ')}" if cols.any?
|
|
57
74
|
end
|
|
58
75
|
end
|
|
59
|
-
end
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
lines << "**
|
|
77
|
+
lines << "**Associations:** #{data[:associations].map { |a| "#{a[:type]} :#{a[:name]}" }.join(', ')}" if data[:associations]&.any?
|
|
78
|
+
lines << "**Validations:** #{data[:validations].map { |v| "#{v[:kind]} on #{v[:attributes].join(', ')}" }.uniq.join('; ')}" if data[:validations]&.any?
|
|
79
|
+
lines << "**Scopes:** #{data[:scopes].join(', ')}" if data[:scopes]&.any?
|
|
80
|
+
lines << "**Enums:** #{data[:enums].map { |k, v| "#{k}: #{v.join(', ')}" }.join('; ')}" if data[:enums]&.any?
|
|
63
81
|
end
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
else
|
|
83
|
+
lines << "## Models" << "_No models matching '#{pattern}'._"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
lines << ""
|
|
87
|
+
matched
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# --- AF: Controllers ---
|
|
91
|
+
def discover_controllers(ctx, pattern, lines)
|
|
92
|
+
controllers = ctx.dig(:controllers, :controllers) || {}
|
|
93
|
+
matched = controllers.select { |name, data| !data[:error] && name.downcase.include?(pattern) }
|
|
94
|
+
|
|
95
|
+
if matched.any?
|
|
96
|
+
lines << "## Controllers (#{matched.size})"
|
|
97
|
+
matched.sort.each do |name, info|
|
|
98
|
+
actions = info[:actions]&.join(", ") || "none"
|
|
99
|
+
lines << "" << "### #{name}"
|
|
100
|
+
lines << "- **Actions:** #{actions}"
|
|
101
|
+
filters = (info[:filters] || []).map do |f|
|
|
102
|
+
label = "#{f[:kind]} #{f[:name]}"
|
|
103
|
+
label += " only: #{f[:only].join(', ')}" if f[:only]&.any?
|
|
104
|
+
label += " except: #{f[:except].join(', ')}" if f[:except]&.any?
|
|
105
|
+
label += " unless: #{f[:unless]}" if f[:unless]
|
|
106
|
+
label
|
|
107
|
+
end
|
|
108
|
+
lines << "- **Filters:** #{filters.join('; ')}" if filters.any?
|
|
66
109
|
end
|
|
67
|
-
|
|
68
|
-
|
|
110
|
+
else
|
|
111
|
+
lines << "## Controllers" << "_No controllers matching '#{pattern}'._"
|
|
112
|
+
end
|
|
113
|
+
lines << ""
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# --- AF: Routes ---
|
|
117
|
+
def discover_routes(ctx, pattern, lines)
|
|
118
|
+
by_controller = ctx.dig(:routes, :by_controller) || {}
|
|
119
|
+
matched = by_controller.select { |ctrl, _| ctrl.downcase.include?(pattern) }
|
|
120
|
+
|
|
121
|
+
if matched.any?
|
|
122
|
+
route_count = matched.values.sum(&:size)
|
|
123
|
+
lines << "## Routes (#{route_count})"
|
|
124
|
+
matched.sort.each do |ctrl, actions|
|
|
125
|
+
actions.each do |r|
|
|
126
|
+
name_part = r[:name] ? " `#{r[:name]}`" : ""
|
|
127
|
+
lines << "- `#{r[:verb]}` `#{r[:path]}` → #{ctrl}##{r[:action]}#{name_part}"
|
|
128
|
+
end
|
|
69
129
|
end
|
|
130
|
+
else
|
|
131
|
+
lines << "## Routes" << "_No routes matching '#{pattern}'._"
|
|
132
|
+
end
|
|
133
|
+
lines << ""
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# --- AF1: Services ---
|
|
137
|
+
def discover_services(root, pattern, lines)
|
|
138
|
+
dir = File.join(root, "app", "services")
|
|
139
|
+
return unless Dir.exist?(dir)
|
|
140
|
+
|
|
141
|
+
found = Dir.glob(File.join(dir, "**", "*.rb")).select do |path|
|
|
142
|
+
File.basename(path, ".rb").include?(pattern) ||
|
|
143
|
+
(File.size(path) < 50_000 && File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace).downcase.include?(pattern))
|
|
144
|
+
end
|
|
145
|
+
return if found.empty?
|
|
146
|
+
|
|
147
|
+
lines << "## Services (#{found.size})"
|
|
148
|
+
found.each do |path|
|
|
149
|
+
relative = path.sub("#{root}/", "")
|
|
150
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
151
|
+
line_count = source.lines.size
|
|
152
|
+
methods = source.scan(/\A\s*def (?:self\.)?(\w+)/m).flatten.reject { |m| m == "initialize" }
|
|
153
|
+
lines << "- `#{relative}` (#{line_count} lines)"
|
|
154
|
+
lines << " Methods: #{methods.first(10).join(', ')}" if methods.any?
|
|
155
|
+
end
|
|
156
|
+
lines << ""
|
|
157
|
+
rescue
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# --- AF2: Jobs ---
|
|
162
|
+
def discover_jobs(root, pattern, lines)
|
|
163
|
+
dir = File.join(root, "app", "jobs")
|
|
164
|
+
return unless Dir.exist?(dir)
|
|
165
|
+
|
|
166
|
+
found = Dir.glob(File.join(dir, "**", "*.rb")).select do |path|
|
|
167
|
+
File.basename(path, ".rb").include?(pattern)
|
|
168
|
+
end
|
|
169
|
+
return if found.empty?
|
|
170
|
+
|
|
171
|
+
lines << "## Jobs (#{found.size})"
|
|
172
|
+
found.each do |path|
|
|
173
|
+
relative = path.sub("#{root}/", "")
|
|
174
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
175
|
+
queue = source.match(/queue_as\s+[:'"](\w+)/)&.captures&.first || "default"
|
|
176
|
+
retries = source.match(/retry_on.*attempts:\s*(\d+)/)&.captures&.first
|
|
177
|
+
lines << "- `#{relative}` (queue: #{queue}#{retries ? ", retries: #{retries}" : ""})"
|
|
178
|
+
end
|
|
179
|
+
lines << ""
|
|
180
|
+
rescue
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# --- AF3: Views + Partials ---
|
|
185
|
+
def discover_views(ctx, root, pattern, lines)
|
|
186
|
+
views_dir = File.join(root, "app", "views")
|
|
187
|
+
return unless Dir.exist?(views_dir)
|
|
188
|
+
|
|
189
|
+
found = Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).select do |path|
|
|
190
|
+
path.sub("#{views_dir}/", "").downcase.include?(pattern)
|
|
70
191
|
end
|
|
71
|
-
|
|
72
|
-
|
|
192
|
+
return if found.empty?
|
|
193
|
+
|
|
194
|
+
lines << "## Views (#{found.size})"
|
|
195
|
+
found.each do |path|
|
|
196
|
+
relative = path.sub("#{views_dir}/", "")
|
|
197
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
198
|
+
line_count = source.lines.size
|
|
199
|
+
partials = source.scan(/render\s+(?:partial:\s*)?["']([^"']+)["']/).flatten
|
|
200
|
+
stimulus = source.scan(/data-controller=["']([^"']+)["']/).flat_map { |m| m.first.split }
|
|
201
|
+
detail = "- `#{relative}` (#{line_count} lines)"
|
|
202
|
+
detail += " renders: #{partials.join(', ')}" if partials.any?
|
|
203
|
+
detail += " stimulus: #{stimulus.join(', ')}" if stimulus.any?
|
|
204
|
+
lines << detail
|
|
205
|
+
end
|
|
206
|
+
lines << ""
|
|
207
|
+
rescue
|
|
208
|
+
nil
|
|
73
209
|
end
|
|
74
210
|
|
|
75
|
-
# --- Controllers ---
|
|
76
|
-
|
|
77
|
-
|
|
211
|
+
# --- AF4: Stimulus Controllers ---
|
|
212
|
+
def discover_stimulus(ctx, pattern, lines)
|
|
213
|
+
stim = ctx[:stimulus]
|
|
214
|
+
return unless stim.is_a?(Hash) && !stim[:error]
|
|
215
|
+
|
|
216
|
+
controllers = stim[:controllers] || []
|
|
217
|
+
matched = controllers.select do |c|
|
|
218
|
+
name = c[:name] || c[:file]&.gsub("_controller.js", "")
|
|
219
|
+
name&.downcase&.include?(pattern)
|
|
220
|
+
end
|
|
221
|
+
return if matched.empty?
|
|
78
222
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
matched_controllers.sort.each do |name, info|
|
|
83
|
-
actions = info[:actions]&.join(", ") || "none"
|
|
84
|
-
filters = (info[:filters] || []).map { |f| "#{f[:kind]} #{f[:name]}" }.join(", ")
|
|
223
|
+
lines << "## Stimulus Controllers (#{matched.size})"
|
|
224
|
+
matched.each do |c|
|
|
225
|
+
name = c[:name] || c[:file]&.gsub("_controller.js", "")
|
|
85
226
|
lines << "" << "### #{name}"
|
|
86
|
-
lines << "- **
|
|
87
|
-
lines << "- **
|
|
227
|
+
lines << "- **Targets:** #{c[:targets].join(', ')}" if c[:targets]&.any?
|
|
228
|
+
lines << "- **Values:** #{c[:values].map { |v| "#{v[:name]}:#{v[:type]}" }.join(', ')}" if c[:values]&.any?
|
|
229
|
+
lines << "- **Actions:** #{c[:actions].join(', ')}" if c[:actions]&.any?
|
|
88
230
|
end
|
|
89
|
-
|
|
90
|
-
lines << "## Controllers" << "_No controllers matching '#{feature}'._"
|
|
231
|
+
lines << ""
|
|
91
232
|
end
|
|
92
233
|
|
|
93
|
-
# ---
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
234
|
+
# --- AF5: Tests ---
|
|
235
|
+
def discover_tests(root, pattern, lines)
|
|
236
|
+
test_dirs = [ File.join(root, "spec"), File.join(root, "test") ]
|
|
237
|
+
found = []
|
|
238
|
+
|
|
239
|
+
test_dirs.each do |dir|
|
|
240
|
+
next unless Dir.exist?(dir)
|
|
241
|
+
Dir.glob(File.join(dir, "**", "*_{test,spec}.rb")).each do |path|
|
|
242
|
+
found << path if File.basename(path, ".rb").include?(pattern)
|
|
243
|
+
end
|
|
244
|
+
Dir.glob(File.join(dir, "**", "{test,spec}_*.rb")).each do |path|
|
|
245
|
+
found << path if File.basename(path, ".rb").include?(pattern)
|
|
105
246
|
end
|
|
106
247
|
end
|
|
107
|
-
|
|
108
|
-
|
|
248
|
+
found.uniq!
|
|
249
|
+
return if found.empty?
|
|
250
|
+
|
|
251
|
+
lines << "## Tests (#{found.size})"
|
|
252
|
+
found.each do |path|
|
|
253
|
+
relative = path.sub("#{root}/", "")
|
|
254
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
255
|
+
test_count = source.scan(/\b(?:it|test|should)\b/).size
|
|
256
|
+
lines << "- `#{relative}` (#{test_count} tests)"
|
|
257
|
+
end
|
|
258
|
+
lines << ""
|
|
259
|
+
rescue
|
|
260
|
+
nil
|
|
109
261
|
end
|
|
110
262
|
|
|
111
|
-
|
|
263
|
+
# --- AF6: Related Models via Associations ---
|
|
264
|
+
def discover_related_models(ctx, matched_models, lines)
|
|
265
|
+
return if matched_models.empty?
|
|
266
|
+
|
|
267
|
+
related = {}
|
|
268
|
+
matched_models.each do |name, data|
|
|
269
|
+
(data[:associations] || []).each do |a|
|
|
270
|
+
related_name = a[:class_name] || a[:name].to_s.classify
|
|
271
|
+
next if matched_models.key?(related_name)
|
|
272
|
+
related[related_name] ||= []
|
|
273
|
+
related[related_name] << "#{a[:type]} from #{name}"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
return if related.empty?
|
|
277
|
+
|
|
278
|
+
lines << "## Related Models (#{related.size})"
|
|
279
|
+
related.sort.each { |name, refs| lines << "- **#{name}** — #{refs.join(', ')}" }
|
|
280
|
+
lines << ""
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# --- AF12: Concern Tracing ---
|
|
284
|
+
def discover_concerns(ctx, matched_models, lines)
|
|
285
|
+
return if matched_models.empty?
|
|
286
|
+
|
|
287
|
+
concerns = {}
|
|
288
|
+
matched_models.each do |_name, data|
|
|
289
|
+
(data[:concerns] || []).each do |c|
|
|
290
|
+
next if c.include?("::") || %w[Kernel JSON PP].include?(c)
|
|
291
|
+
concerns[c] ||= 0
|
|
292
|
+
concerns[c] += 1
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
return if concerns.empty?
|
|
296
|
+
|
|
297
|
+
lines << "## Concerns"
|
|
298
|
+
concerns.sort.each { |name, count| lines << "- **#{name}** (used by #{count} model#{'s' if count > 1})" }
|
|
299
|
+
lines << ""
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# --- AF13: Callback Chains ---
|
|
303
|
+
def discover_callbacks(ctx, matched_models, lines)
|
|
304
|
+
return if matched_models.empty?
|
|
305
|
+
|
|
306
|
+
callbacks = []
|
|
307
|
+
matched_models.each do |name, data|
|
|
308
|
+
(data[:callbacks] || {}).each do |type, methods|
|
|
309
|
+
methods.each { |m| callbacks << "#{name}: #{type} :#{m}" }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
return if callbacks.empty?
|
|
313
|
+
|
|
314
|
+
lines << "## Callbacks"
|
|
315
|
+
callbacks.each { |c| lines << "- #{c}" }
|
|
316
|
+
lines << ""
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# --- AF10: Channels/WebSocket ---
|
|
320
|
+
def discover_channels(root, pattern, lines)
|
|
321
|
+
dir = File.join(root, "app", "channels")
|
|
322
|
+
return unless Dir.exist?(dir)
|
|
323
|
+
|
|
324
|
+
found = Dir.glob(File.join(dir, "**", "*.rb")).select { |p| File.basename(p, ".rb").include?(pattern) }
|
|
325
|
+
return if found.empty?
|
|
326
|
+
|
|
327
|
+
lines << "## Channels (#{found.size})"
|
|
328
|
+
found.each do |path|
|
|
329
|
+
relative = path.sub("#{root}/", "")
|
|
330
|
+
lines << "- `#{relative}`"
|
|
331
|
+
end
|
|
332
|
+
lines << ""
|
|
333
|
+
rescue
|
|
334
|
+
nil
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# --- AF11: Mailers ---
|
|
338
|
+
def discover_mailers(root, pattern, lines)
|
|
339
|
+
dir = File.join(root, "app", "mailers")
|
|
340
|
+
return unless Dir.exist?(dir)
|
|
341
|
+
|
|
342
|
+
found = Dir.glob(File.join(dir, "**", "*.rb")).select { |p| File.basename(p, ".rb").include?(pattern) }
|
|
343
|
+
return if found.empty?
|
|
344
|
+
|
|
345
|
+
lines << "## Mailers (#{found.size})"
|
|
346
|
+
found.each do |path|
|
|
347
|
+
relative = path.sub("#{root}/", "")
|
|
348
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
349
|
+
methods = source.scan(/\A\s*def (\w+)/m).flatten.reject { |m| m == "initialize" }
|
|
350
|
+
lines << "- `#{relative}` — #{methods.join(', ')}" if methods.any?
|
|
351
|
+
end
|
|
352
|
+
lines << ""
|
|
353
|
+
rescue
|
|
354
|
+
nil
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# --- AF9: Environment Dependencies ---
|
|
358
|
+
def discover_env_dependencies(root, pattern, matched_models, lines)
|
|
359
|
+
# Scan services, jobs, and model files for ENV references
|
|
360
|
+
dirs = %w[app/services app/jobs].map { |d| File.join(root, d) }.select { |d| Dir.exist?(d) }
|
|
361
|
+
env_vars = Set.new
|
|
362
|
+
|
|
363
|
+
dirs.each do |dir|
|
|
364
|
+
Dir.glob(File.join(dir, "**", "*.rb")).each do |path|
|
|
365
|
+
next unless File.basename(path, ".rb").include?(pattern) || path.downcase.include?(pattern)
|
|
366
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
367
|
+
source.scan(/ENV\[["']([^"']+)["']\]|ENV\.fetch\(["']([^"']+)["']\)/).each do |m|
|
|
368
|
+
env_vars << (m[0] || m[1])
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
return if env_vars.empty?
|
|
373
|
+
|
|
374
|
+
lines << "## Environment Dependencies"
|
|
375
|
+
env_vars.sort.each { |v| lines << "- `#{v}`" }
|
|
376
|
+
lines << ""
|
|
377
|
+
rescue
|
|
378
|
+
nil
|
|
379
|
+
end
|
|
112
380
|
end
|
|
113
381
|
end
|
|
114
382
|
end
|
|
@@ -72,6 +72,9 @@ module RailsAiContext
|
|
|
72
72
|
# Icons
|
|
73
73
|
lines.concat(render_icons(patterns))
|
|
74
74
|
|
|
75
|
+
# Animations
|
|
76
|
+
lines.concat(render_animations(patterns))
|
|
77
|
+
|
|
75
78
|
# Design tokens
|
|
76
79
|
lines.concat(render_tokens(dt))
|
|
77
80
|
end
|
|
@@ -102,12 +105,14 @@ module RailsAiContext
|
|
|
102
105
|
lines << "- **Borders:** #{scheme[:border_palette].first(4).join(', ')}"
|
|
103
106
|
end
|
|
104
107
|
|
|
105
|
-
# Design token colors
|
|
108
|
+
# Design token colors (DS21: filter oklch noise from summary)
|
|
106
109
|
if dt.is_a?(Hash) && !dt[:error]
|
|
107
110
|
colors = dt.dig(:categorized, :colors) || {}
|
|
108
|
-
|
|
111
|
+
# Filter oklch/complex values in non-full modes — show only hex/rgb/named
|
|
112
|
+
readable_colors = colors.reject { |_, v| v.to_s.match?(/oklch|calc|var\(/) }
|
|
113
|
+
if readable_colors.any?
|
|
109
114
|
lines << "" << "### Token Colors"
|
|
110
|
-
|
|
115
|
+
readable_colors.first(10).each { |name, value| lines << "- `#{name}`: #{value}" }
|
|
111
116
|
end
|
|
112
117
|
end
|
|
113
118
|
|
|
@@ -258,6 +263,19 @@ module RailsAiContext
|
|
|
258
263
|
lines
|
|
259
264
|
end
|
|
260
265
|
|
|
266
|
+
def render_animations(patterns)
|
|
267
|
+
anims = patterns[:animations]
|
|
268
|
+
return [] unless anims.is_a?(Hash) && anims.any?
|
|
269
|
+
|
|
270
|
+
lines = [ "## Animations & Transitions", "" ]
|
|
271
|
+
lines << "- Transitions: #{anims[:transitions].keys.join(', ')}" if anims[:transitions]&.any?
|
|
272
|
+
lines << "- Durations: #{anims[:durations].keys.join(', ')}" if anims[:durations]&.any?
|
|
273
|
+
lines << "- Animations: #{anims[:animates].keys.join(', ')}" if anims[:animates]&.any?
|
|
274
|
+
lines << "- Easing: #{anims[:easing].keys.join(', ')}" if anims[:easing]&.any?
|
|
275
|
+
lines << ""
|
|
276
|
+
lines
|
|
277
|
+
end
|
|
278
|
+
|
|
261
279
|
def render_tokens(dt)
|
|
262
280
|
return [] unless dt.is_a?(Hash) && !dt[:error]
|
|
263
281
|
|
|
@@ -602,14 +602,17 @@ module RailsAiContext
|
|
|
602
602
|
model_data[:associations]&.each { |a| valid << a[:name]; valid << a[:foreign_key] if a[:foreign_key] }
|
|
603
603
|
valid.merge(%w[id _destroy created_at updated_at])
|
|
604
604
|
|
|
605
|
-
#
|
|
606
|
-
|
|
607
|
-
|
|
605
|
+
# V1: Smarter JSONB skip — only skip params matching JSON column names, check the rest
|
|
606
|
+
json_column_names = table_data[:columns]
|
|
607
|
+
&.select { |c| %w[jsonb json].include?(c[:type]) }
|
|
608
|
+
&.map { |c| c[:name] }
|
|
609
|
+
&.to_set || Set.new
|
|
608
610
|
|
|
609
611
|
visitor.permit_calls.each do |pc|
|
|
610
612
|
pc[:params].each do |param|
|
|
611
613
|
next if param.end_with?("_attributes") # nested attributes
|
|
612
614
|
next if valid.include?(param)
|
|
615
|
+
next if json_column_names.include?(param) # param matches a JSON column, skip
|
|
613
616
|
warnings << "permits :#{param} \u2014 not a column in #{table_name} table"
|
|
614
617
|
end
|
|
615
618
|
end
|
|
@@ -678,7 +681,7 @@ module RailsAiContext
|
|
|
678
681
|
action = route[:action]
|
|
679
682
|
next unless action
|
|
680
683
|
unless actions.include?(action)
|
|
681
|
-
warnings << "route #{route[:verb]} #{route[:path]} \u2192 #{action} \u2014 action not found in #{ctrl_class}"
|
|
684
|
+
warnings << "route #{route[:verb]} #{route[:path]} \u2192 #{action} \u2014 action not found in #{ctrl_class}. Fix: add `def #{action}; end` to #{ctrl_class} or remove the route"
|
|
682
685
|
end
|
|
683
686
|
end
|
|
684
687
|
warnings
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -184,6 +184,7 @@ files:
|
|
|
184
184
|
- CONTRIBUTING.md
|
|
185
185
|
- LICENSE
|
|
186
186
|
- README.md
|
|
187
|
+
- ROADMAP.md
|
|
187
188
|
- Rakefile
|
|
188
189
|
- SECURITY.md
|
|
189
190
|
- docs/GUIDE.md
|