@reinteractive/rails-insight 1.0.1

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.
Files changed (90) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +210 -0
  3. package/bin/railsinsight.js +128 -0
  4. package/package.json +62 -0
  5. package/src/core/blast-radius.js +496 -0
  6. package/src/core/constants.js +39 -0
  7. package/src/core/context-loader.js +227 -0
  8. package/src/core/drift-detector.js +168 -0
  9. package/src/core/formatter.js +197 -0
  10. package/src/core/graph.js +510 -0
  11. package/src/core/indexer.js +595 -0
  12. package/src/core/patterns/api.js +27 -0
  13. package/src/core/patterns/auth.js +25 -0
  14. package/src/core/patterns/authorization.js +24 -0
  15. package/src/core/patterns/caching.js +19 -0
  16. package/src/core/patterns/component.js +18 -0
  17. package/src/core/patterns/config.js +15 -0
  18. package/src/core/patterns/controller.js +42 -0
  19. package/src/core/patterns/email.js +20 -0
  20. package/src/core/patterns/factory.js +31 -0
  21. package/src/core/patterns/gemfile.js +9 -0
  22. package/src/core/patterns/helper.js +10 -0
  23. package/src/core/patterns/job.js +12 -0
  24. package/src/core/patterns/model.js +123 -0
  25. package/src/core/patterns/realtime.js +17 -0
  26. package/src/core/patterns/route.js +27 -0
  27. package/src/core/patterns/schema.js +25 -0
  28. package/src/core/patterns/stimulus.js +13 -0
  29. package/src/core/patterns/storage.js +16 -0
  30. package/src/core/patterns/uploader.js +16 -0
  31. package/src/core/patterns/view.js +20 -0
  32. package/src/core/patterns/worker.js +12 -0
  33. package/src/core/patterns.js +27 -0
  34. package/src/core/scanner.js +394 -0
  35. package/src/core/version-detector.js +295 -0
  36. package/src/extractors/api.js +284 -0
  37. package/src/extractors/auth.js +853 -0
  38. package/src/extractors/authorization.js +785 -0
  39. package/src/extractors/caching.js +84 -0
  40. package/src/extractors/component.js +221 -0
  41. package/src/extractors/config.js +81 -0
  42. package/src/extractors/controller.js +273 -0
  43. package/src/extractors/coverage-snapshot.js +296 -0
  44. package/src/extractors/email.js +123 -0
  45. package/src/extractors/factory-registry.js +225 -0
  46. package/src/extractors/gemfile.js +440 -0
  47. package/src/extractors/helper.js +55 -0
  48. package/src/extractors/jobs.js +122 -0
  49. package/src/extractors/model.js +506 -0
  50. package/src/extractors/realtime.js +102 -0
  51. package/src/extractors/routes.js +251 -0
  52. package/src/extractors/schema.js +178 -0
  53. package/src/extractors/stimulus.js +149 -0
  54. package/src/extractors/storage.js +100 -0
  55. package/src/extractors/test-conventions.js +340 -0
  56. package/src/extractors/tier2.js +417 -0
  57. package/src/extractors/tier3.js +84 -0
  58. package/src/extractors/uploader.js +138 -0
  59. package/src/extractors/views.js +131 -0
  60. package/src/extractors/worker.js +62 -0
  61. package/src/git/diff-parser.js +132 -0
  62. package/src/providers/interface.js +12 -0
  63. package/src/providers/local-fs.js +318 -0
  64. package/src/server.js +71 -0
  65. package/src/tools/blast-radius-tools.js +129 -0
  66. package/src/tools/free-tools.js +44 -0
  67. package/src/tools/handlers/get-controller.js +93 -0
  68. package/src/tools/handlers/get-coverage-gaps.js +100 -0
  69. package/src/tools/handlers/get-deep-analysis.js +294 -0
  70. package/src/tools/handlers/get-domain-clusters.js +113 -0
  71. package/src/tools/handlers/get-factory-registry.js +43 -0
  72. package/src/tools/handlers/get-full-index.js +28 -0
  73. package/src/tools/handlers/get-model.js +108 -0
  74. package/src/tools/handlers/get-overview.js +153 -0
  75. package/src/tools/handlers/get-routes.js +18 -0
  76. package/src/tools/handlers/get-schema.js +40 -0
  77. package/src/tools/handlers/get-subgraph.js +82 -0
  78. package/src/tools/handlers/get-test-conventions.js +18 -0
  79. package/src/tools/handlers/get-well-tested-examples.js +51 -0
  80. package/src/tools/handlers/helpers.js +115 -0
  81. package/src/tools/handlers/index-project.js +36 -0
  82. package/src/tools/handlers/search-patterns.js +104 -0
  83. package/src/tools/index.js +34 -0
  84. package/src/tools/pro-tools.js +13 -0
  85. package/src/utils/file-reader.js +20 -0
  86. package/src/utils/inflector.js +223 -0
  87. package/src/utils/ruby-parser.js +115 -0
  88. package/src/utils/spec-style-detector.js +26 -0
  89. package/src/utils/token-counter.js +46 -0
  90. package/src/utils/yaml-parser.js +135 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Test Conventions Extractor
3
+ * Analyses existing spec files to detect testing patterns, styles,
4
+ * and conventions used by the project.
5
+ *
6
+ * @module test-conventions
7
+ */
8
+
9
+ import { detectSpecStyle } from '../utils/spec-style-detector.js'
10
+
11
+ /**
12
+ * Extract test conventions from existing spec files.
13
+ * @param {import('../providers/interface.js').FileProvider} provider
14
+ * @param {Array<{path: string, category: number, categoryName: string, specCategory?: string}>} entries
15
+ * @param {{gems?: object}} gemInfo
16
+ * @returns {object}
17
+ */
18
+ export function extractTestConventions(provider, entries, gemInfo = {}) {
19
+ const gems = gemInfo.gems || {}
20
+
21
+ const result = {
22
+ // Spec file style
23
+ spec_style: detectSpecStyle(entries),
24
+
25
+ // Let style preference
26
+ let_style: null,
27
+ let_count: 0,
28
+ let_bang_count: 0,
29
+
30
+ // Subject usage
31
+ subject_usage: false,
32
+ subject_count: 0,
33
+
34
+ // described_class usage
35
+ described_class_usage: false,
36
+
37
+ // Shared examples
38
+ shared_examples: [],
39
+ shared_examples_count: 0,
40
+
41
+ // Shared contexts
42
+ shared_contexts: [],
43
+ shared_contexts_count: 0,
44
+
45
+ // Custom matchers
46
+ custom_matchers: [],
47
+
48
+ // Authentication helper
49
+ auth_helper: detectAuthHelper(provider, entries, gems),
50
+
51
+ // Database strategy
52
+ database_strategy: detectDatabaseStrategy(provider, gems),
53
+
54
+ // Factory tool
55
+ factory_tool:
56
+ gems.factory_bot_rails || gems.factory_bot
57
+ ? 'factory_bot'
58
+ : gems.fabrication
59
+ ? 'fabrication'
60
+ : null,
61
+
62
+ // Spec file counts by category
63
+ spec_counts: {},
64
+
65
+ // Well-tested files (candidates for pattern reference)
66
+ pattern_reference_files: [],
67
+ }
68
+
69
+ // Scan spec files for convention patterns
70
+ const specEntries = entries.filter(
71
+ (e) => e.categoryName === 'testing' && e.path.endsWith('_spec.rb'),
72
+ )
73
+
74
+ // Count spec files by specCategory
75
+ for (const entry of specEntries) {
76
+ const cat = entry.specCategory || 'other'
77
+ result.spec_counts[cat] = (result.spec_counts[cat] || 0) + 1
78
+ }
79
+
80
+ // Sample up to 20 spec files to detect conventions (avoid reading hundreds)
81
+ const sampleSize = Math.min(specEntries.length, 20)
82
+ const sampledEntries = specEntries.slice(0, sampleSize)
83
+
84
+ for (const entry of sampledEntries) {
85
+ const content = provider.readFile(entry.path)
86
+ if (!content) continue
87
+
88
+ // Let style detection
89
+ const letMatches = (content.match(/^\s*let\s*\(/gm) || []).length
90
+ const letBangMatches = (content.match(/^\s*let!\s*\(/gm) || []).length
91
+ result.let_count += letMatches
92
+ result.let_bang_count += letBangMatches
93
+
94
+ // Subject usage
95
+ if (/^\s*subject\s*[\s{(]/m.test(content)) {
96
+ result.subject_usage = true
97
+ result.subject_count++
98
+ }
99
+
100
+ // described_class usage
101
+ if (/described_class/.test(content)) {
102
+ result.described_class_usage = true
103
+ }
104
+ }
105
+
106
+ // Determine let style
107
+ if (result.let_count > 0 || result.let_bang_count > 0) {
108
+ const ratio =
109
+ result.let_bang_count / (result.let_count + result.let_bang_count)
110
+ if (ratio > 0.7) result.let_style = 'eager'
111
+ else if (ratio < 0.3) result.let_style = 'lazy'
112
+ else result.let_style = 'mixed'
113
+ }
114
+
115
+ // Scan spec/support/ for shared examples, shared contexts, and custom matchers
116
+ const supportEntries = entries.filter(
117
+ (e) => e.path.startsWith('spec/support/') && e.path.endsWith('.rb'),
118
+ )
119
+
120
+ for (const entry of supportEntries) {
121
+ const content = provider.readFile(entry.path)
122
+ if (!content) continue
123
+
124
+ // Shared examples
125
+ const sharedExRe =
126
+ /(?:shared_examples_for|shared_examples|RSpec\.shared_examples)\s+['"]([^'"]+)['"]/g
127
+ let m
128
+ while ((m = sharedExRe.exec(content))) {
129
+ result.shared_examples.push(m[1])
130
+ }
131
+
132
+ // Shared contexts
133
+ const sharedCtxRe =
134
+ /(?:shared_context|RSpec\.shared_context)\s+['"]([^'"]+)['"]/g
135
+ while ((m = sharedCtxRe.exec(content))) {
136
+ result.shared_contexts.push(m[1])
137
+ }
138
+
139
+ // Custom matchers
140
+ const matcherRe = /RSpec::Matchers\.define\s+:(\w+)/g
141
+ while ((m = matcherRe.exec(content))) {
142
+ result.custom_matchers.push(m[1])
143
+ }
144
+
145
+ // Also check for define_negated_matcher
146
+ const negatedRe = /define_negated_matcher\s+:(\w+)/g
147
+ while ((m = negatedRe.exec(content))) {
148
+ result.custom_matchers.push(m[1])
149
+ }
150
+ }
151
+
152
+ // Also check spec/shared_examples/ and spec/shared_contexts/ directories
153
+ const sharedExampleEntries = entries.filter(
154
+ (e) => e.path.startsWith('spec/shared_examples/') && e.path.endsWith('.rb'),
155
+ )
156
+ for (const entry of sharedExampleEntries) {
157
+ const content = provider.readFile(entry.path)
158
+ if (!content) continue
159
+ const re = /(?:shared_examples_for|shared_examples)\s+['"]([^'"]+)['"]/g
160
+ let m
161
+ while ((m = re.exec(content))) {
162
+ result.shared_examples.push(m[1])
163
+ }
164
+ }
165
+
166
+ result.shared_examples_count = result.shared_examples.length
167
+ result.shared_contexts_count = result.shared_contexts.length
168
+
169
+ // Find well-tested files as pattern references
170
+ result.pattern_reference_files = findPatternReferences(provider, specEntries)
171
+
172
+ return result
173
+ }
174
+
175
+ /**
176
+ * Detect authentication test helper.
177
+ * @param {import('../providers/interface.js').FileProvider} provider
178
+ * @param {Array<{path: string}>} entries
179
+ * @param {object} gems
180
+ * @returns {{strategy: string|null, helper_method: string|null, helper_file: string|null, setup_location: string|null}}
181
+ */
182
+ function detectAuthHelper(provider, entries, gems) {
183
+ const result = {
184
+ strategy: null,
185
+ helper_method: null,
186
+ helper_file: null,
187
+ setup_location: null,
188
+ }
189
+
190
+ // Check rails_helper.rb for Devise test helpers
191
+ const railsHelper = provider.readFile('spec/rails_helper.rb')
192
+ if (railsHelper) {
193
+ if (/Devise::Test::IntegrationHelpers/.test(railsHelper)) {
194
+ result.strategy = 'devise'
195
+ result.helper_method = 'sign_in'
196
+ result.helper_file = 'spec/rails_helper.rb'
197
+ result.setup_location = 'spec/rails_helper.rb'
198
+ return result
199
+ }
200
+ if (/Devise::Test::ControllerHelpers/.test(railsHelper)) {
201
+ result.strategy = 'devise_controller'
202
+ result.helper_method = 'sign_in'
203
+ result.helper_file = 'spec/rails_helper.rb'
204
+ result.setup_location = 'spec/rails_helper.rb'
205
+ return result
206
+ }
207
+ if (/Warden::Test::Helpers/.test(railsHelper)) {
208
+ result.strategy = 'warden'
209
+ result.helper_method = 'login_as'
210
+ result.helper_file = 'spec/rails_helper.rb'
211
+ result.setup_location = 'spec/rails_helper.rb'
212
+ return result
213
+ }
214
+ }
215
+
216
+ // Check spec/support/ for custom auth helpers
217
+ const supportEntries = entries.filter(
218
+ (e) =>
219
+ e.path.startsWith('spec/support/') &&
220
+ e.path.endsWith('.rb') &&
221
+ /auth/i.test(e.path),
222
+ )
223
+
224
+ for (const entry of supportEntries) {
225
+ const content = provider.readFile(entry.path)
226
+ if (!content) continue
227
+
228
+ // Look for sign_in method definition
229
+ const signInMatch = content.match(
230
+ /def\s+(sign_in|log_in|login|authenticate)/,
231
+ )
232
+ if (signInMatch) {
233
+ result.strategy = 'custom'
234
+ result.helper_method = signInMatch[1]
235
+ result.helper_file = entry.path
236
+ result.setup_location = entry.path
237
+ return result
238
+ }
239
+ }
240
+
241
+ // Check for JWT/token auth patterns in support files
242
+ for (const entry of supportEntries) {
243
+ const content = provider.readFile(entry.path)
244
+ if (!content) continue
245
+
246
+ if (/auth.*header|bearer|jwt|token/i.test(content)) {
247
+ result.strategy = 'token'
248
+ result.helper_method = null
249
+ result.helper_file = entry.path
250
+ result.setup_location = entry.path
251
+ return result
252
+ }
253
+ }
254
+
255
+ return result
256
+ }
257
+
258
+ /**
259
+ * Detect database cleaning/transaction strategy.
260
+ * @param {import('../providers/interface.js').FileProvider} provider
261
+ * @param {object} gems
262
+ * @returns {{strategy: string|null, config_file: string|null}}
263
+ */
264
+ function detectDatabaseStrategy(provider, gems) {
265
+ const result = {
266
+ strategy: null,
267
+ config_file: null,
268
+ }
269
+
270
+ // Check rails_helper for use_transactional_fixtures
271
+ const railsHelper = provider.readFile('spec/rails_helper.rb') || ''
272
+ if (/use_transactional_fixtures\s*=\s*true/.test(railsHelper)) {
273
+ result.strategy = 'transactional_fixtures'
274
+ result.config_file = 'spec/rails_helper.rb'
275
+ return result
276
+ }
277
+
278
+ // Check for database_cleaner
279
+ if (gems.database_cleaner || gems['database_cleaner-active_record']) {
280
+ result.strategy = 'database_cleaner'
281
+
282
+ // Detect strategy type
283
+ const supportFiles = [
284
+ 'spec/support/database_cleaner.rb',
285
+ 'spec/support/database_cleaner_config.rb',
286
+ ]
287
+ for (const path of supportFiles) {
288
+ const content = provider.readFile(path)
289
+ if (content) {
290
+ result.config_file = path
291
+ if (/strategy\s*=\s*:truncation/.test(content)) {
292
+ result.strategy = 'database_cleaner:truncation'
293
+ } else if (/strategy\s*=\s*:transaction/.test(content)) {
294
+ result.strategy = 'database_cleaner:transaction'
295
+ }
296
+ break
297
+ }
298
+ }
299
+ return result
300
+ }
301
+
302
+ return result
303
+ }
304
+
305
+ /**
306
+ * Find well-structured spec files as pattern references for each category.
307
+ * Selects the spec file with the most describe/context blocks per category.
308
+ * @param {import('../providers/interface.js').FileProvider} provider
309
+ * @param {Array<{path: string, specCategory?: string}>} specEntries
310
+ * @returns {Array<{path: string, category: string, describe_count: number, example_count: number}>}
311
+ */
312
+ function findPatternReferences(provider, specEntries) {
313
+ const byCategory = {}
314
+
315
+ for (const entry of specEntries) {
316
+ const cat = entry.specCategory
317
+ if (!cat || cat === 'factories' || cat === 'support') continue
318
+
319
+ const content = provider.readFile(entry.path)
320
+ if (!content) continue
321
+
322
+ const describeCount = (content.match(/^\s*(?:describe|context)\s/gm) || [])
323
+ .length
324
+ const exampleCount = (content.match(/^\s*it\s/gm) || []).length
325
+
326
+ // Skip trivially small files
327
+ if (exampleCount < 3) continue
328
+
329
+ if (!byCategory[cat] || describeCount > byCategory[cat].describe_count) {
330
+ byCategory[cat] = {
331
+ path: entry.path,
332
+ category: cat,
333
+ describe_count: describeCount,
334
+ example_count: exampleCount,
335
+ }
336
+ }
337
+ }
338
+
339
+ return Object.values(byCategory)
340
+ }