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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e7442b6a28d089867479cba4a99d6f0fcf64e34f692d8c47b576de8e7ec4ffd
4
- data.tar.gz: 0b8b21e8c1581a056afbb03e3ef6b3c45a62c7fbb61d345bbec49d370c4fd191
3
+ metadata.gz: aeeb02e9069ff67b12236ae2f99f169df5b5dd48d42af344ad0734ff1a499307
4
+ data.tar.gz: 0d5f18c57c512747bc06fc063c9e58c95073f288df15f3d74bd0d951815fabe8
5
5
  SHA512:
6
- metadata.gz: e1bd3b280678917a3b94576cef271669fc6892ca32cfd522b88a4c7cee93d73d4301b6d995bd75e78fd64bb448f5e226013b8678411bfd56e68de0a59ccba35b
7
- data.tar.gz: a57fed3f44c4e1599854caf8243a35f62476f487e8b14a41489bcaee1efb4e8398935211ea9073ca70f9a1e34c16a6fa13d79cdaa5cf943d6f0d5d2e82ea100c
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
- return "unknown" unless File.exist?(gemfile)
52
- content = File.read(gemfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
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 content.include?("tailwindcss-rails")
56
+ framework = if gemfile_content.include?("tailwindcss-rails")
55
57
  "tailwind"
56
- elsif content.include?("bootstrap")
58
+ elsif gemfile_content.include?("bootstrap")
57
59
  "bootstrap"
58
- elsif content.include?("dartsass-rails") || content.include?("sassc-rails") || content.include?("sass-rails")
60
+ elsif gemfile_content.include?("dartsass-rails") || gemfile_content.include?("sassc-rails") || gemfile_content.include?("sass-rails")
59
61
  "sass"
60
- elsif content.include?("cssbundling-rails")
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.sort_by(&:name)
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
- (model.methods - ActiveRecord::Base.methods - Object.methods)
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.start_with?("_", "autosave") || scope_names.include?(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
- (model.instance_methods - ActiveRecord::Base.instance_methods - Object.instance_methods)
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") || generated.include?(ms)
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