rails-ai-context 1.2.0 → 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 +36 -0
- data/ROADMAP.md +148 -0
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +38 -0
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +28 -6
- data/lib/rails_ai_context/introspectors/model_introspector.rb +120 -10
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +128 -3
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +20 -26
- data/lib/rails_ai_context/serializers/design_system_helper.rb +47 -0
- data/lib/rails_ai_context/tools/analyze_feature.rb +337 -63
- data/lib/rails_ai_context/tools/get_design_system.rb +21 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +17 -5
- data/lib/rails_ai_context/tools/get_schema.rb +5 -1
- 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,42 @@ 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
|
+
|
|
30
|
+
## [1.2.1] - 2026-03-23
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **New models now discovered via filesystem fallback** — when `ActiveRecord::Base.descendants` misses a newly created model, the introspector scans `app/models/*.rb` and constantizes them. Fixes model invisibility until MCP restart.
|
|
35
|
+
- **Devise meta-methods no longer fill class/instance method caps** — filtered 40+ Devise-generated methods (authentication_keys=, email_regexp=, password_required?, etc.). Source-defined methods now prioritized over reflection-discovered ones.
|
|
36
|
+
- **Controller `unless:`/`if:` conditions now extracted** — filters like `before_action :authenticate_user!, unless: :devise_controller?` now show the condition. Previously silently dropped.
|
|
37
|
+
- **Empty string defaults shown as `""`** — schema tool now renders `""` instead of a blank cell for empty string defaults. AI can distinguish "no default" from "empty string default".
|
|
38
|
+
- **Implicit belongs_to validations labeled** — `presence on user` from `belongs_to :user` now shows `_(implicit from belongs_to)_` and filters phantom `(message: required)` options.
|
|
39
|
+
- **Array columns shown as `type[]`** in generated rules — `string` columns with `array: true` now render as `string[]` in schema rules.
|
|
40
|
+
- **External ID columns no longer hidden** — columns like `paymongo_checkout_id` and `stripe_payment_id` are now shown in schema rules. Only conventional Rails FK columns (matching a table name) are filtered.
|
|
41
|
+
- **Column defaults shown in generated rules** — columns with non-nil defaults now show `(=value)` inline.
|
|
42
|
+
- **`analyze_feature` matches models by table name and underscore form** — `feature:"share"` now finds `CookShare` (via `cook_shares` table and `cook_share` underscore form), not just exact model name substring.
|
|
43
|
+
|
|
8
44
|
## [1.2.0] - 2026-03-23
|
|
9
45
|
|
|
10
46
|
### Added
|
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
|
|
@@ -167,8 +167,14 @@ module RailsAiContext
|
|
|
167
167
|
if (sc = source_constraints[f[:name]])
|
|
168
168
|
f[:only] = sc[:only] if sc[:only]&.any?
|
|
169
169
|
f[:except] = sc[:except] if sc[:except]&.any?
|
|
170
|
+
f[:unless] = sc[:unless] if sc[:unless]
|
|
171
|
+
f[:if] = sc[:if] if sc[:if]
|
|
170
172
|
end
|
|
171
173
|
end
|
|
174
|
+
|
|
175
|
+
# Evaluate known runtime conditions to remove inapplicable filters
|
|
176
|
+
reflection_filters.reject! { |f| filter_excluded_by_condition?(ctrl, f) }
|
|
177
|
+
|
|
172
178
|
return reflection_filters
|
|
173
179
|
end
|
|
174
180
|
end
|
|
@@ -221,6 +227,15 @@ module RailsAiContext
|
|
|
221
227
|
except = parse_action_constraint(line, "except")
|
|
222
228
|
filter[:only] = only if only&.any?
|
|
223
229
|
filter[:except] = except if except&.any?
|
|
230
|
+
|
|
231
|
+
# Extract conditional modifiers (unless:, if:)
|
|
232
|
+
if (unless_match = line.match(/unless:\s*:(\w+[?!]?)/))
|
|
233
|
+
filter[:unless] = unless_match[1]
|
|
234
|
+
end
|
|
235
|
+
if (if_match = line.match(/\bif:\s*:(\w+[?!]?)/))
|
|
236
|
+
filter[:if] = if_match[1]
|
|
237
|
+
end
|
|
238
|
+
|
|
224
239
|
filters << filter
|
|
225
240
|
end
|
|
226
241
|
filters
|
|
@@ -247,6 +262,29 @@ module RailsAiContext
|
|
|
247
262
|
nil
|
|
248
263
|
end
|
|
249
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
|
+
|
|
250
288
|
def extract_action_condition(condition)
|
|
251
289
|
return nil unless condition.is_a?(String) || condition.respond_to?(:to_s)
|
|
252
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|
|
|
@@ -47,11 +47,33 @@ module RailsAiContext
|
|
|
47
47
|
def discover_models
|
|
48
48
|
return [] unless defined?(ActiveRecord::Base)
|
|
49
49
|
|
|
50
|
-
ActiveRecord::Base.descendants.reject do |model|
|
|
50
|
+
models = ActiveRecord::Base.descendants.reject do |model|
|
|
51
51
|
model.abstract_class? ||
|
|
52
52
|
model.name.nil? ||
|
|
53
53
|
config.excluded_models.include?(model.name)
|
|
54
|
-
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Filesystem fallback — discover model files not yet loaded by descendants
|
|
57
|
+
models_dir = File.join(app.root.to_s, "app", "models")
|
|
58
|
+
if Dir.exist?(models_dir)
|
|
59
|
+
known = models.map(&:name).to_set
|
|
60
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |path|
|
|
61
|
+
relative = path.sub("#{models_dir}/", "").sub(/\.rb\z/, "")
|
|
62
|
+
class_name = relative.camelize
|
|
63
|
+
next if known.include?(class_name)
|
|
64
|
+
next if config.excluded_models.include?(class_name)
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
klass = class_name.constantize
|
|
68
|
+
next unless klass < ActiveRecord::Base && !klass.abstract_class?
|
|
69
|
+
models << klass
|
|
70
|
+
rescue NameError, LoadError
|
|
71
|
+
# Not a valid model class
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
models.uniq.sort_by(&:name)
|
|
55
77
|
end
|
|
56
78
|
|
|
57
79
|
def extract_model_details(model)
|
|
@@ -196,29 +218,117 @@ module RailsAiContext
|
|
|
196
218
|
RailsAiContext.configuration.excluded_concerns.any? { |pattern| name.match?(pattern) }
|
|
197
219
|
end
|
|
198
220
|
|
|
221
|
+
DEVISE_CLASS_METHOD_PATTERNS = %w[
|
|
222
|
+
authentication_keys= case_insensitive_keys= strip_whitespace_keys=
|
|
223
|
+
reset_password_keys= confirmation_keys= unlock_keys=
|
|
224
|
+
email_regexp= password_length= timeout_in= remember_for=
|
|
225
|
+
sign_in_after_reset_password= sign_in_after_change_password=
|
|
226
|
+
reconfirmable= extend_remember_period= pepper=
|
|
227
|
+
stretches= allow_unconfirmed_access_for=
|
|
228
|
+
confirm_within= remember_for= unlock_in=
|
|
229
|
+
lock_strategy= unlock_strategy= maximum_attempts=
|
|
230
|
+
paranoid= last_attempt_warning=
|
|
231
|
+
].to_set.freeze
|
|
232
|
+
|
|
199
233
|
def extract_public_class_methods(model)
|
|
200
234
|
scope_names = extract_scopes(model).map(&:to_s)
|
|
201
|
-
|
|
235
|
+
|
|
236
|
+
# Prioritize methods defined in the model's own source file
|
|
237
|
+
source_methods = extract_source_class_methods(model)
|
|
238
|
+
|
|
239
|
+
all_methods = (model.methods - ActiveRecord::Base.methods - Object.methods)
|
|
202
240
|
.reject { |m|
|
|
203
241
|
ms = m.to_s
|
|
204
|
-
ms
|
|
242
|
+
ms == "self" ||
|
|
243
|
+
ms.start_with?("_", "autosave") ||
|
|
244
|
+
scope_names.include?(ms) ||
|
|
245
|
+
DEVISE_CLASS_METHOD_PATTERNS.include?(ms) ||
|
|
246
|
+
ms.end_with?("=") && ms.length > 20 # Devise setter-like methods
|
|
205
247
|
}
|
|
206
|
-
.sort
|
|
207
|
-
.first(30) # Cap to avoid noise
|
|
208
248
|
.map(&:to_s)
|
|
249
|
+
.sort
|
|
250
|
+
|
|
251
|
+
# Source-defined methods first, then reflection-discovered ones
|
|
252
|
+
ordered = source_methods + (all_methods - source_methods)
|
|
253
|
+
ordered.first(30)
|
|
209
254
|
end
|
|
210
255
|
|
|
256
|
+
def extract_source_class_methods(model)
|
|
257
|
+
path = model_source_path(model)
|
|
258
|
+
return [] unless path && File.exist?(path)
|
|
259
|
+
|
|
260
|
+
source = File.read(path)
|
|
261
|
+
methods = []
|
|
262
|
+
in_class_methods = false
|
|
263
|
+
source.each_line do |line|
|
|
264
|
+
in_class_methods = true if line.match?(/\A\s*(?:class << self|def self\.)/)
|
|
265
|
+
if line.match?(/\A\s*def self\.(\w+)/)
|
|
266
|
+
methods << line.match(/def self\.(\w+)/)[1]
|
|
267
|
+
end
|
|
268
|
+
if in_class_methods && line.match?(/\A\s*def (\w+)/)
|
|
269
|
+
methods << line.match(/def (\w+)/)[1]
|
|
270
|
+
end
|
|
271
|
+
in_class_methods = false if in_class_methods && line.match?(/\A\s*end\s*$/) && !line.match?(/def/)
|
|
272
|
+
end
|
|
273
|
+
methods.uniq
|
|
274
|
+
rescue
|
|
275
|
+
[]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
DEVISE_INSTANCE_PATTERNS = %w[
|
|
279
|
+
password_required? email_required? confirmation_required?
|
|
280
|
+
active_for_authentication? inactive_message authenticatable_salt
|
|
281
|
+
after_database_authentication send_devise_notification
|
|
282
|
+
send_confirmation_instructions send_reset_password_instructions
|
|
283
|
+
send_unlock_instructions send_on_create_confirmation_instructions
|
|
284
|
+
devise_mailer clean_up_passwords skip_confirmation!
|
|
285
|
+
skip_reconfirmation! valid_password? update_with_password
|
|
286
|
+
destroy_with_password remember_me! forget_me!
|
|
287
|
+
unauthenticated_message confirmation_period_valid?
|
|
288
|
+
pending_reconfirmation? reconfirmation_required?
|
|
289
|
+
send_email_changed_notification send_password_change_notification
|
|
290
|
+
].to_set.freeze
|
|
291
|
+
|
|
211
292
|
def extract_public_instance_methods(model)
|
|
212
293
|
generated = generated_association_methods(model)
|
|
213
294
|
|
|
214
|
-
|
|
295
|
+
# Prioritize source-defined methods
|
|
296
|
+
source_methods = extract_source_instance_methods(model)
|
|
297
|
+
|
|
298
|
+
all_methods = (model.instance_methods - ActiveRecord::Base.instance_methods - Object.instance_methods)
|
|
215
299
|
.reject { |m|
|
|
216
300
|
ms = m.to_s
|
|
217
|
-
ms.start_with?("_", "autosave", "validate_associated") ||
|
|
301
|
+
ms.start_with?("_", "autosave", "validate_associated") ||
|
|
302
|
+
generated.include?(ms) ||
|
|
303
|
+
DEVISE_INSTANCE_PATTERNS.include?(ms) ||
|
|
304
|
+
ms.match?(/\Awill_save_change_to_|_before_last_save\z|_in_database\z|_before_type_cast\z/)
|
|
218
305
|
}
|
|
219
|
-
.sort
|
|
220
|
-
.first(30)
|
|
221
306
|
.map(&:to_s)
|
|
307
|
+
.sort
|
|
308
|
+
|
|
309
|
+
# Source-defined methods first
|
|
310
|
+
ordered = source_methods + (all_methods - source_methods)
|
|
311
|
+
ordered.first(30)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def extract_source_instance_methods(model)
|
|
315
|
+
path = model_source_path(model)
|
|
316
|
+
return [] unless path && File.exist?(path)
|
|
317
|
+
|
|
318
|
+
source = File.read(path)
|
|
319
|
+
methods = []
|
|
320
|
+
in_private = false
|
|
321
|
+
source.each_line do |line|
|
|
322
|
+
in_private = true if line.match?(/\A\s*private\s*$/)
|
|
323
|
+
next if in_private
|
|
324
|
+
next if line.match?(/\A\s*def self\./)
|
|
325
|
+
if (match = line.match(/\A\s*def (\w+[?!]?)/))
|
|
326
|
+
methods << match[1] unless match[1] == "initialize"
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
methods.uniq
|
|
330
|
+
rescue
|
|
331
|
+
[]
|
|
222
332
|
end
|
|
223
333
|
|
|
224
334
|
# Build list of AR-generated association helper method names to exclude
|