@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.
- package/LICENSE +15 -0
- package/README.md +210 -0
- package/bin/railsinsight.js +128 -0
- package/package.json +62 -0
- package/src/core/blast-radius.js +496 -0
- package/src/core/constants.js +39 -0
- package/src/core/context-loader.js +227 -0
- package/src/core/drift-detector.js +168 -0
- package/src/core/formatter.js +197 -0
- package/src/core/graph.js +510 -0
- package/src/core/indexer.js +595 -0
- package/src/core/patterns/api.js +27 -0
- package/src/core/patterns/auth.js +25 -0
- package/src/core/patterns/authorization.js +24 -0
- package/src/core/patterns/caching.js +19 -0
- package/src/core/patterns/component.js +18 -0
- package/src/core/patterns/config.js +15 -0
- package/src/core/patterns/controller.js +42 -0
- package/src/core/patterns/email.js +20 -0
- package/src/core/patterns/factory.js +31 -0
- package/src/core/patterns/gemfile.js +9 -0
- package/src/core/patterns/helper.js +10 -0
- package/src/core/patterns/job.js +12 -0
- package/src/core/patterns/model.js +123 -0
- package/src/core/patterns/realtime.js +17 -0
- package/src/core/patterns/route.js +27 -0
- package/src/core/patterns/schema.js +25 -0
- package/src/core/patterns/stimulus.js +13 -0
- package/src/core/patterns/storage.js +16 -0
- package/src/core/patterns/uploader.js +16 -0
- package/src/core/patterns/view.js +20 -0
- package/src/core/patterns/worker.js +12 -0
- package/src/core/patterns.js +27 -0
- package/src/core/scanner.js +394 -0
- package/src/core/version-detector.js +295 -0
- package/src/extractors/api.js +284 -0
- package/src/extractors/auth.js +853 -0
- package/src/extractors/authorization.js +785 -0
- package/src/extractors/caching.js +84 -0
- package/src/extractors/component.js +221 -0
- package/src/extractors/config.js +81 -0
- package/src/extractors/controller.js +273 -0
- package/src/extractors/coverage-snapshot.js +296 -0
- package/src/extractors/email.js +123 -0
- package/src/extractors/factory-registry.js +225 -0
- package/src/extractors/gemfile.js +440 -0
- package/src/extractors/helper.js +55 -0
- package/src/extractors/jobs.js +122 -0
- package/src/extractors/model.js +506 -0
- package/src/extractors/realtime.js +102 -0
- package/src/extractors/routes.js +251 -0
- package/src/extractors/schema.js +178 -0
- package/src/extractors/stimulus.js +149 -0
- package/src/extractors/storage.js +100 -0
- package/src/extractors/test-conventions.js +340 -0
- package/src/extractors/tier2.js +417 -0
- package/src/extractors/tier3.js +84 -0
- package/src/extractors/uploader.js +138 -0
- package/src/extractors/views.js +131 -0
- package/src/extractors/worker.js +62 -0
- package/src/git/diff-parser.js +132 -0
- package/src/providers/interface.js +12 -0
- package/src/providers/local-fs.js +318 -0
- package/src/server.js +71 -0
- package/src/tools/blast-radius-tools.js +129 -0
- package/src/tools/free-tools.js +44 -0
- package/src/tools/handlers/get-controller.js +93 -0
- package/src/tools/handlers/get-coverage-gaps.js +100 -0
- package/src/tools/handlers/get-deep-analysis.js +294 -0
- package/src/tools/handlers/get-domain-clusters.js +113 -0
- package/src/tools/handlers/get-factory-registry.js +43 -0
- package/src/tools/handlers/get-full-index.js +28 -0
- package/src/tools/handlers/get-model.js +108 -0
- package/src/tools/handlers/get-overview.js +153 -0
- package/src/tools/handlers/get-routes.js +18 -0
- package/src/tools/handlers/get-schema.js +40 -0
- package/src/tools/handlers/get-subgraph.js +82 -0
- package/src/tools/handlers/get-test-conventions.js +18 -0
- package/src/tools/handlers/get-well-tested-examples.js +51 -0
- package/src/tools/handlers/helpers.js +115 -0
- package/src/tools/handlers/index-project.js +36 -0
- package/src/tools/handlers/search-patterns.js +104 -0
- package/src/tools/index.js +34 -0
- package/src/tools/pro-tools.js +13 -0
- package/src/utils/file-reader.js +20 -0
- package/src/utils/inflector.js +223 -0
- package/src/utils/ruby-parser.js +115 -0
- package/src/utils/spec-style-detector.js +26 -0
- package/src/utils/token-counter.js +46 -0
- package/src/utils/yaml-parser.js +135 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English Inflection Module
|
|
3
|
+
* Provides pluralization, singularization, and case conversion
|
|
4
|
+
* for Rails-style naming conventions.
|
|
5
|
+
*
|
|
6
|
+
* @module inflector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {Array<[RegExp, string]>} Pluralization rules (applied last-to-first). */
|
|
10
|
+
const PLURAL_RULES = [
|
|
11
|
+
[/quiz$/i, 'quizzes'],
|
|
12
|
+
[/^(ox)$/i, '$1en'],
|
|
13
|
+
[/(matr|vert|append)ix$/i, '$1ices'],
|
|
14
|
+
[/(x|ch|ss|sh)$/i, '$1es'],
|
|
15
|
+
[/([^aeiouy])y$/i, '$1ies'],
|
|
16
|
+
[/(hive)$/i, '$1s'],
|
|
17
|
+
[/([lr])f$/i, '$1ves'],
|
|
18
|
+
[/(shea|lea|wol|cal)f$/i, '$1ves'],
|
|
19
|
+
[/([^f])fe$/i, '$1ves'],
|
|
20
|
+
[/sis$/i, 'ses'],
|
|
21
|
+
[/([ti])um$/i, '$1a'],
|
|
22
|
+
[/(buffal|tomat|volcan|potat|ech|her|vet)o$/i, '$1oes'],
|
|
23
|
+
[/(bu|mis|gas)s$/i, '$1ses'],
|
|
24
|
+
[/(alias|status)$/i, '$1es'],
|
|
25
|
+
[/(octop|vir)us$/i, '$1i'],
|
|
26
|
+
[/(ax|test)is$/i, '$1es'],
|
|
27
|
+
[/s$/i, 's'],
|
|
28
|
+
[/$/, 's'],
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/** @type {Array<[RegExp, string]>} Singularization rules (applied last-to-first). */
|
|
32
|
+
const SINGULAR_RULES = [
|
|
33
|
+
[/(database)s$/i, '$1'],
|
|
34
|
+
[/(quiz)zes$/i, '$1'],
|
|
35
|
+
[/(matr)ices$/i, '$1ix'],
|
|
36
|
+
[/(vert|append)ices$/i, '$1ex'],
|
|
37
|
+
[/^(ox)en/i, '$1'],
|
|
38
|
+
[/(alias|status)es$/i, '$1'],
|
|
39
|
+
[/(octop|vir)i$/i, '$1us'],
|
|
40
|
+
[/(cris|ax|test)es$/i, '$1is'],
|
|
41
|
+
[/(shoe)s$/i, '$1'],
|
|
42
|
+
[/(o)es$/i, '$1'],
|
|
43
|
+
[/(bus)es$/i, '$1'],
|
|
44
|
+
[/([mlr])ives$/i, '$1ife'],
|
|
45
|
+
[/(x|ch|ss|sh)es$/i, '$1'],
|
|
46
|
+
[/(m)ovies$/i, '$1ovie'],
|
|
47
|
+
[/(s)eries$/i, '$1eries'],
|
|
48
|
+
[/([^aeiouy])ies$/i, '$1y'],
|
|
49
|
+
[/([lr])ves$/i, '$1f'],
|
|
50
|
+
[/(tive)s$/i, '$1'],
|
|
51
|
+
[/(hive)s$/i, '$1'],
|
|
52
|
+
[/([^f])ves$/i, '$1fe'],
|
|
53
|
+
[/(^analy)ses$/i, '$1sis'],
|
|
54
|
+
[/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '$1$2sis'],
|
|
55
|
+
[/([ti])a$/i, '$1um'],
|
|
56
|
+
[/(n)ews$/i, '$1ews'],
|
|
57
|
+
[/s$/i, ''],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** @type {Array<[string, string]>} Irregular singular/plural pairs. */
|
|
61
|
+
const IRREGULARS = [
|
|
62
|
+
['person', 'people'],
|
|
63
|
+
['man', 'men'],
|
|
64
|
+
['woman', 'women'],
|
|
65
|
+
['child', 'children'],
|
|
66
|
+
['sex', 'sexes'],
|
|
67
|
+
['move', 'moves'],
|
|
68
|
+
['zombie', 'zombies'],
|
|
69
|
+
['goose', 'geese'],
|
|
70
|
+
['mouse', 'mice'],
|
|
71
|
+
['tooth', 'teeth'],
|
|
72
|
+
['foot', 'feet'],
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
/** @type {Set<string>} Words that do not change between singular and plural. */
|
|
76
|
+
const UNCOUNTABLES = new Set([
|
|
77
|
+
'equipment',
|
|
78
|
+
'information',
|
|
79
|
+
'rice',
|
|
80
|
+
'money',
|
|
81
|
+
'species',
|
|
82
|
+
'series',
|
|
83
|
+
'fish',
|
|
84
|
+
'sheep',
|
|
85
|
+
'jeans',
|
|
86
|
+
'police',
|
|
87
|
+
'news',
|
|
88
|
+
'data',
|
|
89
|
+
'feedback',
|
|
90
|
+
'staff',
|
|
91
|
+
'advice',
|
|
92
|
+
'furniture',
|
|
93
|
+
'homework',
|
|
94
|
+
'knowledge',
|
|
95
|
+
'luggage',
|
|
96
|
+
'progress',
|
|
97
|
+
'research',
|
|
98
|
+
'software',
|
|
99
|
+
'weather',
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a word is uncountable (case-insensitive).
|
|
104
|
+
* @param {string} word
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isUncountable(word) {
|
|
108
|
+
return UNCOUNTABLES.has(word.toLowerCase())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check irregular words in the given direction.
|
|
113
|
+
* @param {string} word
|
|
114
|
+
* @param {'toPlural'|'toSingular'} direction
|
|
115
|
+
* @returns {string|null} Replacement word or null
|
|
116
|
+
*/
|
|
117
|
+
function checkIrregular(word, direction) {
|
|
118
|
+
const lower = word.toLowerCase()
|
|
119
|
+
for (const [singular, plural] of IRREGULARS) {
|
|
120
|
+
const from = direction === 'toPlural' ? singular : plural
|
|
121
|
+
const to = direction === 'toPlural' ? plural : singular
|
|
122
|
+
if (lower === from) {
|
|
123
|
+
return preserveCase(word, to)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Preserve the first-letter casing of the original word on the replacement.
|
|
131
|
+
* @param {string} original
|
|
132
|
+
* @param {string} replacement
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
function preserveCase(original, replacement) {
|
|
136
|
+
if (!original || !replacement) return replacement
|
|
137
|
+
if (original[0] === original[0].toUpperCase()) {
|
|
138
|
+
return replacement.charAt(0).toUpperCase() + replacement.slice(1)
|
|
139
|
+
}
|
|
140
|
+
return replacement
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Apply inflection rules to a word (first match wins, most specific first).
|
|
145
|
+
* @param {string} word
|
|
146
|
+
* @param {Array<[RegExp, string]>} rules
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
function applyRules(word, rules) {
|
|
150
|
+
for (const [pattern, replacement] of rules) {
|
|
151
|
+
if (pattern.test(word)) {
|
|
152
|
+
return word.replace(pattern, replacement)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return word
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Pluralize an English word.
|
|
160
|
+
* @param {string} word - Singular English word
|
|
161
|
+
* @returns {string} Plural form
|
|
162
|
+
*/
|
|
163
|
+
export function pluralize(word) {
|
|
164
|
+
if (!word) return ''
|
|
165
|
+
if (isUncountable(word)) return word
|
|
166
|
+
const irregular = checkIrregular(word, 'toPlural')
|
|
167
|
+
if (irregular) return irregular
|
|
168
|
+
return applyRules(word, PLURAL_RULES)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Singularize an English word.
|
|
173
|
+
* @param {string} word - Plural English word
|
|
174
|
+
* @returns {string} Singular form
|
|
175
|
+
*/
|
|
176
|
+
export function singularize(word) {
|
|
177
|
+
if (!word) return ''
|
|
178
|
+
if (isUncountable(word)) return word
|
|
179
|
+
const irregular = checkIrregular(word, 'toSingular')
|
|
180
|
+
if (irregular) return irregular
|
|
181
|
+
return applyRules(word, SINGULAR_RULES)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convert a PascalCase string to snake_case.
|
|
186
|
+
* @param {string} str
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export function underscore(str) {
|
|
190
|
+
if (!str) return ''
|
|
191
|
+
return str
|
|
192
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
193
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert a snake_case or plural string to a PascalCase singular class name.
|
|
199
|
+
* 'user_profiles' → 'UserProfile', 'comments' → 'Comment'
|
|
200
|
+
* @param {string} str - snake_case or plural string
|
|
201
|
+
* @returns {string} PascalCase singular class name
|
|
202
|
+
*/
|
|
203
|
+
export function classify(str) {
|
|
204
|
+
if (!str) return ''
|
|
205
|
+
return str
|
|
206
|
+
.split(/[_\s]+/)
|
|
207
|
+
.map((segment, idx, arr) => {
|
|
208
|
+
const word = idx === arr.length - 1 ? singularize(segment) : segment
|
|
209
|
+
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
210
|
+
})
|
|
211
|
+
.join('')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convert a PascalCase class name to a snake_case plural table name.
|
|
216
|
+
* 'UserProfile' → 'user_profiles', 'Person' → 'people'
|
|
217
|
+
* @param {string} className - PascalCase class name
|
|
218
|
+
* @returns {string} snake_case plural table name
|
|
219
|
+
*/
|
|
220
|
+
export function tableize(className) {
|
|
221
|
+
if (!className) return ''
|
|
222
|
+
return pluralize(underscore(className))
|
|
223
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex-based Ruby declaration extractor.
|
|
3
|
+
* All functions accept string content (not file paths).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract class declaration from Ruby source.
|
|
8
|
+
* @param {string} content - Ruby file content
|
|
9
|
+
* @returns {{ name: string, superclass: string|null }|null}
|
|
10
|
+
*/
|
|
11
|
+
export function extractClassDeclaration(content) {
|
|
12
|
+
const match = content.match(/class\s+(\w+(?:::\w+)*)\s*<\s*(\w+(?:::\w+)*)/)
|
|
13
|
+
if (match) {
|
|
14
|
+
return { name: match[1], superclass: match[2] }
|
|
15
|
+
}
|
|
16
|
+
// Class without superclass
|
|
17
|
+
const basic = content.match(/class\s+(\w+(?:::\w+)*)\s*$/m)
|
|
18
|
+
if (basic) {
|
|
19
|
+
return { name: basic[1], superclass: null }
|
|
20
|
+
}
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract module declaration from Ruby source.
|
|
26
|
+
* @param {string} content
|
|
27
|
+
* @returns {string|null} Module name
|
|
28
|
+
*/
|
|
29
|
+
export function extractModuleDeclaration(content) {
|
|
30
|
+
const match = content.match(/module\s+(\w+(?:::\w+)*)/)
|
|
31
|
+
return match ? match[1] : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract all method names from Ruby source.
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
export function extractMethodNames(content) {
|
|
40
|
+
const methods = []
|
|
41
|
+
const regex = /^\s*def\s+(?:self\.)?(\w+[?!=]?)/gm
|
|
42
|
+
let match
|
|
43
|
+
while ((match = regex.exec(content)) !== null) {
|
|
44
|
+
methods.push(match[1])
|
|
45
|
+
}
|
|
46
|
+
return methods
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract DSL calls (like has_many, validates, scope, etc).
|
|
51
|
+
* @param {string} content
|
|
52
|
+
* @param {RegExp} pattern - The DSL pattern to match
|
|
53
|
+
* @returns {RegExpExecArray[]} All matches
|
|
54
|
+
*/
|
|
55
|
+
export function extractDSLCalls(content, pattern) {
|
|
56
|
+
const results = []
|
|
57
|
+
const regex = new RegExp(
|
|
58
|
+
pattern.source,
|
|
59
|
+
pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g',
|
|
60
|
+
)
|
|
61
|
+
let match
|
|
62
|
+
while ((match = regex.exec(content)) !== null) {
|
|
63
|
+
results.push(match)
|
|
64
|
+
}
|
|
65
|
+
return results
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract include and extend statements.
|
|
70
|
+
* @param {string} content
|
|
71
|
+
* @returns {{ includes: string[], extends: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
export function extractIncludesExtends(content) {
|
|
74
|
+
const includes = []
|
|
75
|
+
const extends_ = []
|
|
76
|
+
|
|
77
|
+
const includeRegex = /^\s*include\s+(\w+(?:::\w+)*)/gm
|
|
78
|
+
let match
|
|
79
|
+
while ((match = includeRegex.exec(content)) !== null) {
|
|
80
|
+
includes.push(match[1])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const extendRegex = /^\s*extend\s+(\w+(?:::\w+)*)/gm
|
|
84
|
+
while ((match = extendRegex.exec(content)) !== null) {
|
|
85
|
+
extends_.push(match[1])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { includes, extends: extends_ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract the visibility sections (public/private/protected) from Ruby source.
|
|
93
|
+
* Returns methods grouped by visibility.
|
|
94
|
+
* @param {string} content
|
|
95
|
+
* @returns {{ public: string[], private: string[], protected: string[] }}
|
|
96
|
+
*/
|
|
97
|
+
export function extractMethodsByVisibility(content) {
|
|
98
|
+
const result = { public: [], private: [], protected: [] }
|
|
99
|
+
let currentVisibility = 'public'
|
|
100
|
+
const lines = content.split('\n')
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const visMatch = line.match(/^\s*(private|protected)\s*$/)
|
|
104
|
+
if (visMatch) {
|
|
105
|
+
currentVisibility = visMatch[1]
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
const methodMatch = line.match(/^\s*def\s+(?:self\.)?(\w+[?!=]?)/)
|
|
109
|
+
if (methodMatch) {
|
|
110
|
+
result[currentVisibility].push(methodMatch[1])
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared spec style detection utility.
|
|
3
|
+
* Detects whether a project uses request specs or controller specs.
|
|
4
|
+
*
|
|
5
|
+
* @module spec-style-detector
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect spec style (request vs controller specs).
|
|
10
|
+
* @param {Array<{path: string}>} entries
|
|
11
|
+
* @returns {{primary: string, request_count: number, controller_count: number, has_mixed: boolean}}
|
|
12
|
+
*/
|
|
13
|
+
export function detectSpecStyle(entries) {
|
|
14
|
+
const requestCount = entries.filter((e) =>
|
|
15
|
+
e.path.startsWith('spec/requests/'),
|
|
16
|
+
).length
|
|
17
|
+
const controllerCount = entries.filter((e) =>
|
|
18
|
+
e.path.startsWith('spec/controllers/'),
|
|
19
|
+
).length
|
|
20
|
+
return {
|
|
21
|
+
primary: requestCount >= controllerCount ? 'request' : 'controller',
|
|
22
|
+
request_count: requestCount,
|
|
23
|
+
controller_count: controllerCount,
|
|
24
|
+
has_mixed: requestCount > 0 && controllerCount > 0,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimation utilities.
|
|
3
|
+
* Uses content-aware character-per-token ratios for more accurate estimation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Characters-per-token ratio for different content types. */
|
|
7
|
+
const CHARS_PER_TOKEN_PROSE = 4.0
|
|
8
|
+
const CHARS_PER_TOKEN_JSON = 3.0
|
|
9
|
+
const CHARS_PER_TOKEN_CODE = 3.5
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect content type and return appropriate chars-per-token ratio.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function detectContentRatio(text) {
|
|
17
|
+
if (text.length < 10) return CHARS_PER_TOKEN_PROSE
|
|
18
|
+
const sample = text.slice(0, 200)
|
|
19
|
+
const jsonIndicators = (sample.match(/[{}\[\]:,"]/g) || []).length
|
|
20
|
+
const ratio = jsonIndicators / sample.length
|
|
21
|
+
if (ratio > 0.15) return CHARS_PER_TOKEN_JSON
|
|
22
|
+
if (ratio > 0.05) return CHARS_PER_TOKEN_CODE
|
|
23
|
+
return CHARS_PER_TOKEN_PROSE
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Estimate tokens for a text string.
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @returns {number}
|
|
30
|
+
*/
|
|
31
|
+
export function estimateTokens(text) {
|
|
32
|
+
if (!text) return 0
|
|
33
|
+
const ratio = detectContentRatio(text)
|
|
34
|
+
return Math.ceil(text.length / ratio)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Estimate tokens for a JSON-serializable object.
|
|
39
|
+
* @param {Object} obj
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
export function estimateTokensForObject(obj) {
|
|
43
|
+
if (obj === null || obj === undefined) return 0
|
|
44
|
+
const json = JSON.stringify(obj)
|
|
45
|
+
return estimateTokens(json)
|
|
46
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple YAML parser for Rails config files.
|
|
3
|
+
* Handles key-value pairs, arrays, nested structures, and anchors/aliases.
|
|
4
|
+
* Not a full YAML parser — designed for Rails convention YAML files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse a simple YAML string into a nested object.
|
|
9
|
+
* @param {string} content - YAML content
|
|
10
|
+
* @returns {Object}
|
|
11
|
+
*/
|
|
12
|
+
export function parseYaml(content) {
|
|
13
|
+
if (!content || typeof content !== 'string') return {}
|
|
14
|
+
|
|
15
|
+
const lines = content.split('\n')
|
|
16
|
+
const result = {}
|
|
17
|
+
const anchors = {}
|
|
18
|
+
const stack = [{ obj: result, indent: -1 }]
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i]
|
|
22
|
+
|
|
23
|
+
// Skip empty lines and comments
|
|
24
|
+
if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue
|
|
25
|
+
|
|
26
|
+
// Handle ERB tags by removing them
|
|
27
|
+
const cleanLine = line.replace(/<%.*?%>/g, '')
|
|
28
|
+
if (/^\s*$/.test(cleanLine)) continue
|
|
29
|
+
|
|
30
|
+
const indentMatch = cleanLine.match(/^(\s*)/)
|
|
31
|
+
const indent = indentMatch ? indentMatch[1].length : 0
|
|
32
|
+
|
|
33
|
+
// Pop stack to correct parent
|
|
34
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
35
|
+
stack.pop()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Merge key: <<: *alias
|
|
39
|
+
const mergeMatch = cleanLine.match(/^\s*<<:\s*\*(\w+)/)
|
|
40
|
+
if (mergeMatch) {
|
|
41
|
+
const aliasName = mergeMatch[1]
|
|
42
|
+
const source = anchors[aliasName]
|
|
43
|
+
if (source && typeof source === 'object') {
|
|
44
|
+
const parent = stack[stack.length - 1].obj
|
|
45
|
+
Object.assign(parent, structuredClone(source))
|
|
46
|
+
}
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Array item
|
|
51
|
+
const arrayMatch = cleanLine.match(/^(\s*)-\s*(.+)$/)
|
|
52
|
+
if (arrayMatch) {
|
|
53
|
+
const parent = stack[stack.length - 1]
|
|
54
|
+
if (parent.arrayKey) {
|
|
55
|
+
if (!Array.isArray(parent.obj[parent.arrayKey])) {
|
|
56
|
+
parent.obj[parent.arrayKey] = []
|
|
57
|
+
}
|
|
58
|
+
parent.obj[parent.arrayKey].push(parseYamlValue(arrayMatch[2].trim()))
|
|
59
|
+
}
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Key-value with anchor: key: &anchor value
|
|
64
|
+
const kvAnchorMatch = cleanLine.match(
|
|
65
|
+
/^(\s*)(\w[\w\s-]*):\s*&(\w+)\s*(.*)$/,
|
|
66
|
+
)
|
|
67
|
+
if (kvAnchorMatch) {
|
|
68
|
+
const key = kvAnchorMatch[2].trim()
|
|
69
|
+
const anchorName = kvAnchorMatch[3]
|
|
70
|
+
const value = kvAnchorMatch[4].trim()
|
|
71
|
+
const parent = stack[stack.length - 1].obj
|
|
72
|
+
|
|
73
|
+
if (value === '' || value === '|' || value === '>') {
|
|
74
|
+
parent[key] = {}
|
|
75
|
+
anchors[anchorName] = parent[key]
|
|
76
|
+
stack.push({ obj: parent[key], indent, arrayKey: null })
|
|
77
|
+
} else {
|
|
78
|
+
parent[key] = parseYamlValue(value)
|
|
79
|
+
anchors[anchorName] = parent[key]
|
|
80
|
+
}
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Key-value with alias: key: *alias
|
|
85
|
+
const kvAliasMatch = cleanLine.match(/^(\s*)(\w[\w\s-]*):\s*\*(\w+)/)
|
|
86
|
+
if (kvAliasMatch) {
|
|
87
|
+
const key = kvAliasMatch[2].trim()
|
|
88
|
+
const aliasName = kvAliasMatch[3]
|
|
89
|
+
const parent = stack[stack.length - 1].obj
|
|
90
|
+
const source = anchors[aliasName]
|
|
91
|
+
parent[key] = source !== undefined ? structuredClone(source) : null
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Key-value pair
|
|
96
|
+
const kvMatch = cleanLine.match(/^(\s*)(\w[\w\s-]*):\s*(.*)$/)
|
|
97
|
+
if (kvMatch) {
|
|
98
|
+
const key = kvMatch[2].trim()
|
|
99
|
+
const value = kvMatch[3].trim()
|
|
100
|
+
const parent = stack[stack.length - 1].obj
|
|
101
|
+
|
|
102
|
+
if (value === '' || value === '|' || value === '>') {
|
|
103
|
+
parent[key] = {}
|
|
104
|
+
stack.push({ obj: parent[key], indent, arrayKey: null })
|
|
105
|
+
} else {
|
|
106
|
+
parent[key] = parseYamlValue(value)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Resolve anchors: for any anchor that points to an object, update the reference
|
|
112
|
+
// since nested keys were added after the anchor was stored
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse a YAML value string into appropriate JS type.
|
|
118
|
+
* @param {string} value
|
|
119
|
+
* @returns {string|number|boolean|null}
|
|
120
|
+
*/
|
|
121
|
+
function parseYamlValue(value) {
|
|
122
|
+
if (value === 'true') return true
|
|
123
|
+
if (value === 'false') return false
|
|
124
|
+
if (value === 'null' || value === '~') return null
|
|
125
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10)
|
|
126
|
+
if (/^\d+\.\d+$/.test(value)) return parseFloat(value)
|
|
127
|
+
// Remove quotes
|
|
128
|
+
if (
|
|
129
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
130
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
131
|
+
) {
|
|
132
|
+
return value.slice(1, -1)
|
|
133
|
+
}
|
|
134
|
+
return value
|
|
135
|
+
}
|