@reinteractive/rails-insight 1.0.5 → 1.0.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reinteractive/rails-insight",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Rails-aware codebase indexer — MCP server for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,6 +5,82 @@
5
5
 
6
6
  import { MODEL_PATTERNS } from '../core/patterns.js'
7
7
 
8
+ /**
9
+ * Join lines where a declaration continues on the next line (ends with comma).
10
+ * This handles multi-line belongs_to, has_many, etc. options.
11
+ * @param {string} content
12
+ * @returns {string}
13
+ */
14
+ const STATEMENT_START =
15
+ /^(?:belongs_to|has_many|has_one|has_and_belongs_to_many|has_and_belongs|scope\s|validates?\s|def\s|class\s|module\s|end\b|include\s|extend\s|enum\s|before_|after_|around_|delegate\s|attr_|#|private\b|protected\b|accepts_nested)/
16
+
17
+ function joinContinuationLines(content) {
18
+ const lines = content.split('\n')
19
+ const joined = []
20
+ for (let i = 0; i < lines.length; i++) {
21
+ if (
22
+ joined.length > 0 &&
23
+ joined[joined.length - 1].trimEnd().endsWith(',')
24
+ ) {
25
+ const nextTrimmed = lines[i].trim()
26
+ if (nextTrimmed && !STATEMENT_START.test(nextTrimmed)) {
27
+ joined[joined.length - 1] =
28
+ joined[joined.length - 1].trimEnd() + ' ' + nextTrimmed
29
+ continue
30
+ }
31
+ }
32
+ joined.push(lines[i])
33
+ }
34
+ return joined.join('\n')
35
+ }
36
+
37
+ /**
38
+ * Extract scope body using brace-balanced scanning (handles nested braces and
39
+ * multi-line lambda/proc bodies).
40
+ * @param {string} content
41
+ * @returns {Record<string, string>} map of scope name → body string
42
+ */
43
+ function extractScopeBodies(content) {
44
+ const result = {}
45
+ const lines = content.split('\n')
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const declMatch = lines[i].match(
48
+ /^\s*scope\s+:(\w+),\s*(?:->|lambda|proc)/,
49
+ )
50
+ if (!declMatch) continue
51
+ const name = declMatch[1]
52
+ // Scan forward to find brace-balanced body
53
+ let depth = 0
54
+ let started = false
55
+ const bodyChars = []
56
+ outer: for (let j = i; j < lines.length; j++) {
57
+ const line = j === i ? lines[j] : lines[j]
58
+ for (const ch of line) {
59
+ if (ch === '{') {
60
+ depth++
61
+ if (depth === 1) {
62
+ started = true
63
+ continue // skip the opening brace itself
64
+ }
65
+ }
66
+ if (ch === '}') {
67
+ depth--
68
+ if (depth === 0 && started) break outer
69
+ }
70
+ if (started) bodyChars.push(ch)
71
+ }
72
+ if (started && depth > 0) bodyChars.push(' ')
73
+ }
74
+ if (bodyChars.length > 0) {
75
+ result[name] = bodyChars
76
+ .join('')
77
+ .replace(/\s+/g, ' ')
78
+ .trim()
79
+ }
80
+ }
81
+ return result
82
+ }
83
+
8
84
  /**
9
85
  * Extract all model information from a single model file.
10
86
  * @param {import('../providers/interface.js').FileProvider} provider
@@ -56,8 +132,9 @@ export function extractModel(provider, filePath, className) {
56
132
  }
57
133
  }
58
134
 
59
- // Associations
135
+ // Associations (join continuation lines first so multi-line options are captured)
60
136
  const associations = []
137
+ const assocContent = joinContinuationLines(content)
61
138
  const assocTypes = [
62
139
  { key: 'belongsTo', type: 'belongs_to' },
63
140
  { key: 'hasMany', type: 'has_many' },
@@ -66,7 +143,7 @@ export function extractModel(provider, filePath, className) {
66
143
  ]
67
144
  for (const { key, type } of assocTypes) {
68
145
  const re = new RegExp(MODEL_PATTERNS[key].source, 'gm')
69
- while ((m = re.exec(content))) {
146
+ while ((m = re.exec(assocContent))) {
70
147
  const entry = { type, name: m[1], options: m[2] || null }
71
148
  // Check for through
72
149
  if (entry.options) {
@@ -102,17 +179,16 @@ export function extractModel(provider, filePath, className) {
102
179
  // Scopes — names array (backward-compat) + scope_queries dict with bodies
103
180
  const scopes = []
104
181
  const scope_queries = {}
105
- // Extended pattern: capture the body inside { } after ->
106
- const scopeBodyRe =
107
- /^\s*scope\s+:(\w+),\s*->\s*(?:\([^)]*\)\s*)?\{\s*([^}]+)\}/gm
108
- const scopeSimpleRe = new RegExp(MODEL_PATTERNS.scope.source, 'gm')
109
182
  const scopeNamesFound = new Set()
110
- while ((m = scopeBodyRe.exec(content))) {
111
- scopes.push(m[1])
112
- scope_queries[m[1]] = m[2].trim().replace(/\s+/g, ' ')
113
- scopeNamesFound.add(m[1])
183
+ // Use brace-balanced extractor for scope bodies (handles multi-line and nested braces)
184
+ const extractedBodies = extractScopeBodies(content)
185
+ for (const [name, body] of Object.entries(extractedBodies)) {
186
+ scopes.push(name)
187
+ scope_queries[name] = body
188
+ scopeNamesFound.add(name)
114
189
  }
115
190
  // Fall back to name-only for scopes we couldn't extract a body from
191
+ const scopeSimpleRe = new RegExp(MODEL_PATTERNS.scope.source, 'gm')
116
192
  while ((m = scopeSimpleRe.exec(content))) {
117
193
  if (!scopeNamesFound.has(m[1])) scopes.push(m[1])
118
194
  }
@@ -366,6 +442,14 @@ export function extractModel(provider, filePath, className) {
366
442
 
367
443
  // Audited
368
444
  const audited = MODEL_PATTERNS.audited.test(content)
445
+ const has_associated_audits = /^\s*has_associated_audits/m.test(content)
446
+
447
+ // accepts_nested_attributes_for
448
+ const nested_attributes = []
449
+ const nestedAttrsRe = /^\s*accepts_nested_attributes_for\s+:(\w+)(?:,\s*(.+))?$/gm
450
+ while ((m = nestedAttrsRe.exec(content))) {
451
+ nested_attributes.push({ name: m[1], options: m[2]?.trim() || null })
452
+ }
369
453
 
370
454
  // STI base detection (has subclasses inheriting from this, detected elsewhere)
371
455
  const sti_base = false
@@ -396,7 +480,7 @@ export function extractModel(provider, filePath, className) {
396
480
  continue
397
481
  }
398
482
 
399
- const mm = line.match(/^\s*def\s+(\w+[?!=]?)/)
483
+ const mm = line.match(/^\s*def\s+((?:self\.)?\w+[?!=]?)/)
400
484
  if (mm) {
401
485
  // Close previous method
402
486
  if (currentMethodName && !inPrivate) {
@@ -485,6 +569,8 @@ export function extractModel(provider, filePath, className) {
485
569
  state_machine,
486
570
  paper_trail,
487
571
  audited,
572
+ has_associated_audits,
573
+ nested_attributes,
488
574
  public_methods,
489
575
  method_line_ranges,
490
576
  }