@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,294 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ noIndex,
4
+ respond,
5
+ toTableName,
6
+ pathToClassName,
7
+ SIGNIFICANT_CATEGORIES,
8
+ DROP_GEMS,
9
+ NOTABLE_ABSENT_CANDIDATES,
10
+ } from './helpers.js'
11
+
12
+ /**
13
+ * Register the get_deep_analysis tool.
14
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
15
+ * @param {Object} state - Mutable state with { index, provider, verbose }
16
+ */
17
+ export function register(server, state) {
18
+ server.tool(
19
+ 'get_deep_analysis',
20
+ 'Get deep analysis for a specific category. Categories: authentication, authorization, jobs, email, storage, caching, realtime, api_patterns, dependencies, components, stimulus, views, convention_drift, manifest, detected_stack, related, model_list, controller_list, component_list, testing, design_patterns, test_conventions, factory_registry, coverage_snapshot',
21
+ {
22
+ category: z.string().describe('The category to analyze'),
23
+ name: z
24
+ .string()
25
+ .optional()
26
+ .describe(
27
+ 'Entity name for categories that accept it (e.g. model name, component name)',
28
+ ),
29
+ depth: z
30
+ .number()
31
+ .optional()
32
+ .describe('Depth for related queries (default: 2)'),
33
+ },
34
+ async ({ category, name, depth = 2 }) => {
35
+ if (!state.index) return noIndex()
36
+ const index = state.index
37
+ const extractions = index.extractions || {}
38
+
39
+ switch (category) {
40
+ case 'authentication':
41
+ return respond(extractions.auth || {})
42
+
43
+ case 'authorization':
44
+ return respond(extractions.authorization || {})
45
+
46
+ case 'jobs':
47
+ return respond(extractions.jobs || {})
48
+
49
+ case 'email':
50
+ return respond(extractions.email || {})
51
+
52
+ case 'storage':
53
+ return respond(extractions.storage || {})
54
+
55
+ case 'caching':
56
+ return respond(extractions.caching || {})
57
+
58
+ case 'realtime':
59
+ return respond(extractions.realtime || {})
60
+
61
+ case 'api_patterns':
62
+ return respond(extractions.api || {})
63
+
64
+ case 'dependencies': {
65
+ const gemfile = extractions.gemfile || {}
66
+ const allGems = Array.isArray(gemfile.gems) ? gemfile.gems : []
67
+ const gemNames = new Set(allGems.map((g) => g.name))
68
+ const significant = {}
69
+ for (const gem of allGems) {
70
+ if (!SIGNIFICANT_CATEGORIES.has(gem.category)) continue
71
+ if (DROP_GEMS.has(gem.name)) continue
72
+ if (!significant[gem.category]) significant[gem.category] = {}
73
+ significant[gem.category][gem.name] =
74
+ gem.resolved || gem.version || 'unknown'
75
+ }
76
+ if (significant.testing) {
77
+ const testingEntries = Object.entries(significant.testing).slice(
78
+ 0,
79
+ 3,
80
+ )
81
+ significant.testing = Object.fromEntries(testingEntries)
82
+ }
83
+ const notableAbsent = NOTABLE_ABSENT_CANDIDATES.filter(
84
+ (g) => !gemNames.has(g),
85
+ )
86
+ return respond({
87
+ ...significant,
88
+ total_gem_count: allGems.length,
89
+ notable_absent: notableAbsent,
90
+ ruby_version: gemfile.rubyVersion || null,
91
+ })
92
+ }
93
+
94
+ case 'components': {
95
+ const components = extractions.components || {}
96
+ if (name) {
97
+ const comp = components[name]
98
+ if (!comp)
99
+ return respond({
100
+ error: `Component '${name}' not found`,
101
+ available: Object.keys(components),
102
+ })
103
+ return respond(comp)
104
+ }
105
+ return respond(
106
+ Object.entries(components).map(([n, c]) => ({
107
+ name: n,
108
+ tier: c.tier,
109
+ slot_count: (c.slots || []).length,
110
+ has_preview: c.has_preview || false,
111
+ file: c.file,
112
+ })),
113
+ )
114
+ }
115
+
116
+ case 'stimulus': {
117
+ const stimulusControllers = extractions.stimulus_controllers || []
118
+ if (name) {
119
+ const sc = stimulusControllers.find(
120
+ (s) => s.identifier === name || s.class === name,
121
+ )
122
+ if (!sc)
123
+ return respond({
124
+ error: `Stimulus controller '${name}' not found`,
125
+ })
126
+ return respond(sc)
127
+ }
128
+ return respond(stimulusControllers)
129
+ }
130
+
131
+ case 'views':
132
+ return respond(extractions.views || {})
133
+
134
+ case 'convention_drift':
135
+ return respond({
136
+ drift: index.drift || [],
137
+ total: (index.drift || []).length,
138
+ })
139
+
140
+ case 'manifest': {
141
+ const manifest = index.manifest || {}
142
+ if (name) {
143
+ const entries = manifest.byCategory?.[name] || []
144
+ return respond({
145
+ category: name,
146
+ count: entries.length,
147
+ files: entries.map((e) => e.path),
148
+ })
149
+ }
150
+ return respond({
151
+ total_files: manifest.total_files,
152
+ categories: manifest.stats,
153
+ })
154
+ }
155
+
156
+ case 'detected_stack':
157
+ return respond(index.versions || {})
158
+
159
+ case 'related': {
160
+ if (!name)
161
+ return respond({
162
+ error: 'name parameter required for related category',
163
+ })
164
+ const allRels = index.relationships || []
165
+ const rankings = index.rankings || {}
166
+ const visited = new Set([name])
167
+ let frontier = [name]
168
+ const connected = []
169
+ for (let d = 0; d < depth && frontier.length > 0; d++) {
170
+ const nextFrontier = []
171
+ for (const current of frontier) {
172
+ for (const rel of allRels) {
173
+ let neighbor = null,
174
+ direction = null
175
+ if (rel.from === current && !visited.has(rel.to)) {
176
+ neighbor = rel.to
177
+ direction = 'outgoing'
178
+ } else if (rel.to === current && !visited.has(rel.from)) {
179
+ neighbor = rel.from
180
+ direction = 'incoming'
181
+ }
182
+ if (neighbor) {
183
+ visited.add(neighbor)
184
+ nextFrontier.push(neighbor)
185
+ connected.push({
186
+ entity: neighbor,
187
+ relationship: rel.type,
188
+ direction,
189
+ distance: d + 1,
190
+ rank: rankings[neighbor] || 0,
191
+ })
192
+ }
193
+ }
194
+ }
195
+ frontier = nextFrontier
196
+ }
197
+ connected.sort((a, b) => a.distance - b.distance || b.rank - a.rank)
198
+ return respond({
199
+ source: name,
200
+ depth,
201
+ connected,
202
+ total: connected.length,
203
+ })
204
+ }
205
+
206
+ case 'model_list': {
207
+ const models = extractions.models || {}
208
+ return respond(
209
+ Object.entries(models).map(([n, m]) => ({
210
+ name: n,
211
+ superclass: m.superclass || 'ApplicationRecord',
212
+ association_count: (m.associations || []).length,
213
+ scope_count: (m.scopes || []).length,
214
+ has_secure_password: m.has_secure_password || false,
215
+ file: m.file,
216
+ })),
217
+ )
218
+ }
219
+
220
+ case 'controller_list': {
221
+ const controllers = extractions.controllers || {}
222
+ return respond(
223
+ Object.entries(controllers).map(([n, c]) => ({
224
+ name: n,
225
+ superclass: c.superclass || 'ApplicationController',
226
+ action_count: (c.actions || []).length,
227
+ namespace: c.namespace || null,
228
+ file: c.file,
229
+ })),
230
+ )
231
+ }
232
+
233
+ case 'component_list': {
234
+ const components = extractions.components || {}
235
+ return respond(
236
+ Object.entries(components).map(([n, c]) => ({
237
+ name: n,
238
+ tier: c.tier,
239
+ slot_count: (c.slots || []).length,
240
+ has_preview: c.has_preview || false,
241
+ file: c.file,
242
+ })),
243
+ )
244
+ }
245
+
246
+ case 'testing':
247
+ return respond(extractions.tier2?.testing || {})
248
+
249
+ case 'design_patterns':
250
+ return respond(extractions.tier2?.design_patterns || {})
251
+
252
+ case 'test_conventions':
253
+ return respond(extractions.test_conventions || {})
254
+
255
+ case 'factory_registry':
256
+ return respond(extractions.factory_registry || {})
257
+
258
+ case 'coverage_snapshot':
259
+ return respond(extractions.coverage_snapshot || {})
260
+
261
+ default:
262
+ return respond({
263
+ error: `Unknown category: ${category}`,
264
+ available: [
265
+ 'authentication',
266
+ 'authorization',
267
+ 'jobs',
268
+ 'email',
269
+ 'storage',
270
+ 'caching',
271
+ 'realtime',
272
+ 'api_patterns',
273
+ 'dependencies',
274
+ 'components',
275
+ 'stimulus',
276
+ 'views',
277
+ 'convention_drift',
278
+ 'manifest',
279
+ 'detected_stack',
280
+ 'related',
281
+ 'model_list',
282
+ 'controller_list',
283
+ 'component_list',
284
+ 'testing',
285
+ 'design_patterns',
286
+ 'test_conventions',
287
+ 'factory_registry',
288
+ 'coverage_snapshot',
289
+ ],
290
+ })
291
+ }
292
+ },
293
+ )
294
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod'
2
+ import { noIndex, respond, pathToClassName } from './helpers.js'
3
+ import { WELL_COVERED_THRESHOLD } from '../../core/constants.js'
4
+
5
+ /**
6
+ * Register the get_domain_clusters tool.
7
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
8
+ * @param {Object} state - Mutable state with { index, provider, verbose }
9
+ */
10
+ export function register(server, state) {
11
+ server.tool(
12
+ 'get_domain_clusters',
13
+ 'Returns domain-clustered file groups for parallel test generation. Files in the same cluster share associations and factories. Files in different clusters can be worked on simultaneously without conflict.',
14
+ {
15
+ max_cluster_size: z
16
+ .number()
17
+ .optional()
18
+ .describe('Maximum files per cluster (default: 8)'),
19
+ include_covered: z
20
+ .boolean()
21
+ .optional()
22
+ .describe('Include files that already have coverage (default: false)'),
23
+ },
24
+ async ({ max_cluster_size = 8, include_covered = false }) => {
25
+ if (!state.index) return noIndex()
26
+ const extractions = state.index.extractions || {}
27
+ const models = extractions.models || {}
28
+ const coverageSnapshot = extractions.coverage_snapshot || {}
29
+ const factoryRegistry = extractions.factory_registry || {}
30
+ const relationships = state.index.relationships || []
31
+
32
+ const clusters = []
33
+ const assigned = new Set()
34
+
35
+ const sortedModels = Object.entries(models)
36
+ .filter(([, m]) => m.type !== 'concern' && !m.abstract)
37
+ .sort(
38
+ (a, b) =>
39
+ (b[1].associations?.length || 0) - (a[1].associations?.length || 0),
40
+ )
41
+
42
+ for (const [name, model] of sortedModels) {
43
+ if (assigned.has(name)) continue
44
+
45
+ if (!include_covered) {
46
+ const fileCov = coverageSnapshot.per_file?.[model.file]
47
+ if (fileCov && fileCov.line_coverage >= WELL_COVERED_THRESHOLD)
48
+ continue
49
+ }
50
+
51
+ const cluster = {
52
+ anchor: name,
53
+ models: [name],
54
+ files: model.file ? [model.file] : [],
55
+ factories_available: [],
56
+ shared_associations: [],
57
+ }
58
+ assigned.add(name)
59
+
60
+ const relatedModels = (model.associations || [])
61
+ .map((a) => pathToClassName(a.name))
62
+ .filter((n) => models[n] && !assigned.has(n))
63
+
64
+ for (const related of relatedModels) {
65
+ if (cluster.models.length >= max_cluster_size) break
66
+
67
+ if (!include_covered) {
68
+ const relModel = models[related]
69
+ const fileCov = relModel?.file
70
+ ? coverageSnapshot.per_file?.[relModel.file]
71
+ : null
72
+ if (fileCov && fileCov.line_coverage >= WELL_COVERED_THRESHOLD)
73
+ continue
74
+ }
75
+
76
+ cluster.models.push(related)
77
+ assigned.add(related)
78
+ if (models[related]?.file) cluster.files.push(models[related].file)
79
+ cluster.shared_associations.push({
80
+ from: name,
81
+ to: related,
82
+ type:
83
+ (model.associations || []).find(
84
+ (a) => pathToClassName(a.name) === related,
85
+ )?.type || 'association',
86
+ })
87
+ }
88
+
89
+ for (const modelName of cluster.models) {
90
+ const factoryName = modelName.replace(/([A-Z])/g, (m, l, i) =>
91
+ i === 0 ? l.toLowerCase() : `_${l.toLowerCase()}`,
92
+ )
93
+ if (factoryRegistry.factories?.[factoryName]) {
94
+ cluster.factories_available.push(factoryName)
95
+ }
96
+ }
97
+
98
+ clusters.push(cluster)
99
+ }
100
+
101
+ return respond({
102
+ clusters,
103
+ total_clusters: clusters.length,
104
+ unassigned_models: Object.keys(models).filter(
105
+ (n) =>
106
+ !assigned.has(n) &&
107
+ models[n].type !== 'concern' &&
108
+ !models[n].abstract,
109
+ ).length,
110
+ })
111
+ },
112
+ )
113
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod'
2
+ import { noIndex, respond } from './helpers.js'
3
+
4
+ /**
5
+ * Register the get_factory_registry tool.
6
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
7
+ * @param {Object} state - Mutable state with { index, provider, verbose }
8
+ */
9
+ export function register(server, state) {
10
+ server.tool(
11
+ 'get_factory_registry',
12
+ 'Returns parsed FactoryBot factory definitions including attributes, traits, sequences, and associations. Use to understand what test data factories are available.',
13
+ {
14
+ model: z
15
+ .string()
16
+ .optional()
17
+ .describe('Filter to a specific model/factory name'),
18
+ },
19
+ async ({ model }) => {
20
+ if (!state.index) return noIndex()
21
+ const registry = state.index.extractions?.factory_registry || {}
22
+
23
+ if (model) {
24
+ const factory =
25
+ registry.factories?.[model] ||
26
+ registry.factories?.[
27
+ model.replace(/([A-Z])/g, (m, l, i) =>
28
+ i === 0 ? l.toLowerCase() : `_${l.toLowerCase()}`,
29
+ )
30
+ ]
31
+ if (!factory) {
32
+ return respond({
33
+ error: `Factory for '${model}' not found`,
34
+ available: Object.keys(registry.factories || {}),
35
+ })
36
+ }
37
+ return respond(factory)
38
+ }
39
+
40
+ return respond(registry)
41
+ },
42
+ )
43
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod'
2
+ import { formatOutput } from '../../core/formatter.js'
3
+ import { noIndex, respond } from './helpers.js'
4
+ import { DEFAULT_FULL_INDEX_BUDGET } from '../../core/constants.js'
5
+
6
+ /**
7
+ * Register the get_full_index tool.
8
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
9
+ * @param {Object} state - Mutable state with { index, provider, verbose }
10
+ */
11
+ export function register(server, state) {
12
+ server.tool(
13
+ 'get_full_index',
14
+ 'Complete index JSON trimmed to fit a specified token budget.',
15
+ {
16
+ token_budget: z
17
+ .number()
18
+ .optional()
19
+ .default(DEFAULT_FULL_INDEX_BUDGET)
20
+ .describe('Maximum token budget (default: 12000)'),
21
+ },
22
+ async ({ token_budget = DEFAULT_FULL_INDEX_BUDGET }) => {
23
+ if (!state.index) return noIndex()
24
+ const trimmed = formatOutput(state.index, token_budget)
25
+ return respond(trimmed)
26
+ },
27
+ )
28
+ }
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod'
2
+ import { noIndex, respond, toTableName, pathToClassName } from './helpers.js'
3
+
4
+ /**
5
+ * Register the get_model tool.
6
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
7
+ * @param {Object} state - Mutable state with { index, provider, verbose }
8
+ */
9
+ export function register(server, state) {
10
+ server.tool(
11
+ 'get_model',
12
+ 'Deep extraction for a specific model: associations, validations, scopes with queries, enums with values, callbacks, public methods, database columns.',
13
+ { name: z.string().describe('Model class name (e.g. "User")') },
14
+ async ({ name }) => {
15
+ if (!state.index) return noIndex()
16
+ const models = state.index.extractions?.models || {}
17
+ const model = models[name]
18
+ if (!model) {
19
+ return respond({
20
+ error: `Model '${name}' not found`,
21
+ available: Object.keys(models),
22
+ })
23
+ }
24
+
25
+ // Enrich with schema columns
26
+ const schema = state.index.extractions?.schema || {}
27
+ const tables = schema.tables || []
28
+ const tableName = model.table_name || toTableName(name)
29
+ const tableData = tables.find((t) => t.name === tableName)
30
+ const columns = tableData
31
+ ? tableData.columns.map((c) => ({
32
+ name: c.name,
33
+ type: c.type,
34
+ constraints: c.constraints || null,
35
+ ...(tableData.indexes?.some(
36
+ (i) => i.columns.includes(c.name) && i.unique,
37
+ )
38
+ ? { unique_index: true }
39
+ : {}),
40
+ }))
41
+ : null
42
+
43
+ // FK relationships from schema
44
+ const schemaFks = schema.foreign_keys || []
45
+ const foreign_keys = schemaFks
46
+ .filter((fk) => fk.from_table === tableName)
47
+ .map((fk) => ({
48
+ column: fk.column,
49
+ references: { table: fk.to_table, column: fk.primary_key || 'id' },
50
+ }))
51
+
52
+ // Indexes for this table
53
+ const indexes = (tableData?.indexes || []).map((i) => ({
54
+ columns: i.columns,
55
+ unique: i.unique || false,
56
+ name: i.name || null,
57
+ }))
58
+
59
+ // Inverse associations
60
+ const allModels = state.index.extractions?.models || {}
61
+ const nameLower = name.toLowerCase()
62
+ const inverse_associations = Object.entries(allModels)
63
+ .filter(([mName]) => mName !== name)
64
+ .flatMap(([mName, m]) =>
65
+ (m.associations || [])
66
+ .filter((a) => {
67
+ const assocName = a.name?.toLowerCase?.() || ''
68
+ return (
69
+ assocName === nameLower ||
70
+ assocName === nameLower + 's' ||
71
+ assocName === nameLower + 'es' ||
72
+ (a.options &&
73
+ String(a.options)
74
+ .toLowerCase()
75
+ .includes(`class_name.*${nameLower}`))
76
+ )
77
+ })
78
+ .map((a) => ({
79
+ model: mName,
80
+ type: a.type,
81
+ name: a.name,
82
+ options: a.options || null,
83
+ })),
84
+ )
85
+
86
+ // Auth relevance disambiguation
87
+ let auth_relevance = undefined
88
+ const authzData = state.index.extractions?.authorization || {}
89
+ if (
90
+ /^role$/i.test(name) &&
91
+ authzData.roles?.model &&
92
+ authzData.roles.model !== name
93
+ ) {
94
+ auth_relevance = `none — this is a domain model for job positions, not related to access control. Authorization roles are defined as an enum on the ${authzData.roles.model} model.`
95
+ }
96
+
97
+ return respond({
98
+ ...model,
99
+ columns,
100
+ indexes: indexes.length > 0 ? indexes : null,
101
+ foreign_keys: foreign_keys.length > 0 ? foreign_keys : null,
102
+ inverse_associations:
103
+ inverse_associations.length > 0 ? inverse_associations : null,
104
+ ...(auth_relevance ? { auth_relevance } : {}),
105
+ })
106
+ },
107
+ )
108
+ }