ruvector 0.2.18 → 0.2.20

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.
@@ -0,0 +1,438 @@
1
+ /**
2
+ * style-improver.js - Readability improvements and JSDoc generation.
3
+ *
4
+ * Transforms beautified-but-minified code into human-readable form:
5
+ * - Converts minification artifacts (!0, !1, void 0)
6
+ * - Adds blank lines between declarations
7
+ * - Converts optional chaining candidates
8
+ * - Generates JSDoc comments from context
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /**
14
+ * Apply all readability improvements to source code.
15
+ *
16
+ * @param {string} source
17
+ * @param {object} [options]
18
+ * @param {boolean} [options.convertBooleans=true]
19
+ * @param {boolean} [options.convertVoid=true]
20
+ * @param {boolean} [options.optionalChaining=true]
21
+ * @param {boolean} [options.addSpacing=true]
22
+ * @param {boolean} [options.expandCommaExpressions=true]
23
+ * @returns {string}
24
+ */
25
+ function improveReadability(source, options = {}) {
26
+ const {
27
+ convertBooleans = true,
28
+ convertVoid = true,
29
+ optionalChaining = true,
30
+ addSpacing = true,
31
+ expandCommaExpressions = true,
32
+ } = options;
33
+
34
+ let result = source;
35
+
36
+ if (convertBooleans) {
37
+ result = convertMinifiedBooleans(result);
38
+ }
39
+ if (convertVoid) {
40
+ result = convertVoidZero(result);
41
+ }
42
+ if (optionalChaining) {
43
+ result = convertToOptionalChaining(result);
44
+ }
45
+ if (expandCommaExpressions) {
46
+ result = expandCommaExprs(result);
47
+ }
48
+ if (addSpacing) {
49
+ result = addBlankLines(result);
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Convert !0 -> true, !1 -> false.
57
+ * Must not match inside strings or comments.
58
+ *
59
+ * @param {string} source
60
+ * @returns {string}
61
+ */
62
+ function convertMinifiedBooleans(source) {
63
+ // Replace !0 with true (not inside strings)
64
+ let result = source.replace(/(?<![a-zA-Z0-9_$"'`])!0(?![a-zA-Z0-9_$])/g, 'true');
65
+ // Replace !1 with false
66
+ result = result.replace(/(?<![a-zA-Z0-9_$"'`])!1(?![a-zA-Z0-9_$])/g, 'false');
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Convert void 0 -> undefined.
72
+ *
73
+ * @param {string} source
74
+ * @returns {string}
75
+ */
76
+ function convertVoidZero(source) {
77
+ return source.replace(/\bvoid 0\b/g, 'undefined');
78
+ }
79
+
80
+ /**
81
+ * Convert guard chains to optional chaining.
82
+ * a && a.b && a.b.c -> a?.b?.c
83
+ * a && a.b -> a?.b
84
+ *
85
+ * Only applies to simple property access chains (no method calls).
86
+ *
87
+ * @param {string} source
88
+ * @returns {string}
89
+ */
90
+ function convertToOptionalChaining(source) {
91
+ // Pattern: a && a.b && a.b.c
92
+ // Match the longest chains first
93
+ let result = source;
94
+
95
+ // 3-level chain: a && a.b && a.b.c
96
+ result = result.replace(
97
+ /\b([a-zA-Z_$]\w*)(?:\s*&&\s*\1\.([a-zA-Z_$]\w*))(?:\s*&&\s*\1\.\2\.([a-zA-Z_$]\w*))/g,
98
+ '$1?.$2?.$3',
99
+ );
100
+
101
+ // 2-level chain: a && a.b
102
+ result = result.replace(
103
+ /\b([a-zA-Z_$]\w*)\s*&&\s*\1\.([a-zA-Z_$]\w*)/g,
104
+ '$1?.$2',
105
+ );
106
+
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * Expand simple comma expressions into separate statements.
112
+ * Only expands top-level comma expressions (not inside for-loops, function args, etc.).
113
+ *
114
+ * a = 1, b = 2, c = 3 -> a = 1;\nb = 2;\nc = 3
115
+ *
116
+ * @param {string} source
117
+ * @returns {string}
118
+ */
119
+ function expandCommaExprs(source) {
120
+ const lines = source.split('\n');
121
+ const result = [];
122
+
123
+ for (const line of lines) {
124
+ const trimmed = line.trim();
125
+
126
+ // Skip lines that are clearly not comma expressions
127
+ if (trimmed.startsWith('for') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
128
+ result.push(line);
129
+ continue;
130
+ }
131
+
132
+ // Skip function calls, array literals, object literals
133
+ if (trimmed.includes('(') || trimmed.startsWith('{') || trimmed.startsWith('[')) {
134
+ result.push(line);
135
+ continue;
136
+ }
137
+
138
+ // Check for assignment comma expressions: a=1,b=2,c=3
139
+ if (/^\s*[a-zA-Z_$]\w*\s*=.*,.*[a-zA-Z_$]\w*\s*=/.test(trimmed)) {
140
+ const indent = line.match(/^(\s*)/)[1];
141
+ // Split on comma but only between assignments
142
+ const parts = splitCommaExpression(trimmed);
143
+ if (parts.length > 1) {
144
+ for (const part of parts) {
145
+ result.push(indent + part.trim() + ';');
146
+ }
147
+ continue;
148
+ }
149
+ }
150
+
151
+ result.push(line);
152
+ }
153
+
154
+ return result.join('\n');
155
+ }
156
+
157
+ /**
158
+ * Split a comma expression into parts, respecting nested parens/brackets.
159
+ * @param {string} expr
160
+ * @returns {string[]}
161
+ */
162
+ function splitCommaExpression(expr) {
163
+ const parts = [];
164
+ let depth = 0;
165
+ let current = '';
166
+
167
+ for (let i = 0; i < expr.length; i++) {
168
+ const ch = expr[i];
169
+ if (ch === '(' || ch === '[' || ch === '{') {
170
+ depth++;
171
+ current += ch;
172
+ } else if (ch === ')' || ch === ']' || ch === '}') {
173
+ depth--;
174
+ current += ch;
175
+ } else if (ch === ',' && depth === 0) {
176
+ parts.push(current);
177
+ current = '';
178
+ } else {
179
+ current += ch;
180
+ }
181
+ }
182
+ if (current.trim()) parts.push(current);
183
+
184
+ // Only return split if each part looks like an assignment
185
+ const allAssignments = parts.every((p) => /\s*[a-zA-Z_$]\w*\s*=/.test(p.trim()));
186
+ if (allAssignments && parts.length > 1) return parts;
187
+ return [expr];
188
+ }
189
+
190
+ /**
191
+ * Add blank lines between top-level declarations for readability.
192
+ *
193
+ * @param {string} source
194
+ * @returns {string}
195
+ */
196
+ function addBlankLines(source) {
197
+ const lines = source.split('\n');
198
+ const result = [];
199
+ let prevWasDecl = false;
200
+
201
+ for (let i = 0; i < lines.length; i++) {
202
+ const trimmed = lines[i].trim();
203
+ const isDecl =
204
+ /^(function|async function|class|const|let|var|export)/.test(trimmed) &&
205
+ !trimmed.startsWith('const {') &&
206
+ trimmed.length > 20;
207
+
208
+ // Add blank line before a new declaration block
209
+ if (isDecl && prevWasDecl && result.length > 0 && result[result.length - 1].trim() !== '') {
210
+ // Only add if there is not already a blank line
211
+ result.push('');
212
+ }
213
+
214
+ result.push(lines[i]);
215
+
216
+ // Track if we just left a closing brace (end of function/class)
217
+ if (trimmed === '}' || trimmed === '};') {
218
+ prevWasDecl = true;
219
+ } else if (trimmed === '') {
220
+ prevWasDecl = false;
221
+ } else {
222
+ prevWasDecl = isDecl;
223
+ }
224
+ }
225
+
226
+ return result.join('\n');
227
+ }
228
+
229
+ /**
230
+ * Generate a JSDoc comment for a function/class declaration.
231
+ *
232
+ * @param {string} declaration - the declaration line(s)
233
+ * @param {string[]} contextStrings - strings found near the declaration
234
+ * @param {object} [options]
235
+ * @param {Array<{oldName: string, newName: string}>} [options.renames] - applied renames
236
+ * @returns {string|null} JSDoc comment string, or null if not applicable
237
+ */
238
+ function generateJSDoc(declaration, contextStrings, options = {}) {
239
+ const { renames = [] } = options;
240
+
241
+ // Determine declaration type
242
+ const isAsyncGen = /async\s+function\s*\*/.test(declaration);
243
+ const isGenerator = /function\s*\*/.test(declaration);
244
+ const isAsync = /async/.test(declaration);
245
+ const isClass = /class\s+/.test(declaration);
246
+ const isFunction =
247
+ /function[\s*]/.test(declaration) || /=>\s*{/.test(declaration) || /=>\s*[^{]/.test(declaration);
248
+
249
+ if (!isFunction && !isClass) return null;
250
+
251
+ const lines = ['/**'];
252
+
253
+ // Infer purpose from context
254
+ const purpose = inferPurpose(declaration, contextStrings, renames);
255
+ if (purpose) {
256
+ lines.push(` * ${purpose}`);
257
+ }
258
+
259
+ if (isClass) {
260
+ const extendsMatch = declaration.match(/extends\s+(\w+)/);
261
+ if (extendsMatch) {
262
+ lines.push(` * @extends ${extendsMatch[1]}`);
263
+ }
264
+ lines.push(' */');
265
+ return lines.join('\n');
266
+ }
267
+
268
+ // Extract parameters
269
+ const params = extractParams(declaration);
270
+ if (params.length > 0) {
271
+ for (const param of params) {
272
+ const type = inferParamType(param, contextStrings);
273
+ lines.push(` * @param {${type}} ${param}`);
274
+ }
275
+ }
276
+
277
+ // Yields for generators
278
+ if (isAsyncGen || isGenerator) {
279
+ const yieldType = inferYieldType(declaration, contextStrings);
280
+ lines.push(` * @yields {${yieldType}}`);
281
+ }
282
+
283
+ // Returns
284
+ if (!isAsyncGen && !isGenerator) {
285
+ const returnType = inferReturnType(declaration, contextStrings, isAsync);
286
+ if (returnType) {
287
+ lines.push(` * @returns {${returnType}}`);
288
+ }
289
+ }
290
+
291
+ lines.push(' */');
292
+ return lines.join('\n');
293
+ }
294
+
295
+ /**
296
+ * Infer the purpose of a function from its context.
297
+ * @param {string} declaration
298
+ * @param {string[]} context
299
+ * @param {Array<{oldName: string, newName: string}>} renames
300
+ * @returns {string|null}
301
+ */
302
+ function inferPurpose(declaration, context, renames) {
303
+ const ctx = context.join(' ').toLowerCase();
304
+ const decl = declaration.toLowerCase();
305
+
306
+ // Use the renamed function name as a hint
307
+ const funcNameMatch = declaration.match(/function\s+(\w+)|(\w+)\s*[=:]\s*(async\s+)?function/);
308
+ const funcName = funcNameMatch ? (funcNameMatch[1] || funcNameMatch[2]) : null;
309
+
310
+ if (funcName) {
311
+ // Convert camelCase to sentence
312
+ const words = funcName.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
313
+ if (words.length > 3 && !/^[a-z]$/.test(words)) {
314
+ return capitalizeFirst(words) + '.';
315
+ }
316
+ }
317
+
318
+ // Context-based purpose
319
+ if (ctx.includes('stream') && ctx.includes('event')) return 'Process streaming events from the API.';
320
+ if (ctx.includes('permission') && ctx.includes('check')) return 'Check if the operation is permitted.';
321
+ if (ctx.includes('compact') && ctx.includes('token')) return 'Compact context to fit within token budget.';
322
+ if (ctx.includes('tool') && ctx.includes('dispatch')) return 'Dispatch tool invocations to handlers.';
323
+ if (ctx.includes('mcp') && ctx.includes('connect')) return 'Establish MCP server connection.';
324
+ if (ctx.includes('fetch') && ctx.includes('api')) return 'Make an API request.';
325
+ if (ctx.includes('parse') && ctx.includes('json')) return 'Parse JSON response data.';
326
+ if (ctx.includes('validate') && ctx.includes('input')) return 'Validate input parameters.';
327
+ if (ctx.includes('error') && ctx.includes('handle')) return 'Handle and format error responses.';
328
+
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Extract parameter names from a function declaration.
334
+ * @param {string} declaration
335
+ * @returns {string[]}
336
+ */
337
+ function extractParams(declaration) {
338
+ // Match function parameters: function name(a, b, c) or (a, b) =>
339
+ const match = declaration.match(/\(([^)]*)\)/);
340
+ if (!match || !match[1].trim()) return [];
341
+
342
+ return match[1]
343
+ .split(',')
344
+ .map((p) => p.trim())
345
+ .filter((p) => p.length > 0)
346
+ .map((p) => {
347
+ // Handle destructuring: { a, b } -> "options"
348
+ if (p.startsWith('{')) return 'options';
349
+ if (p.startsWith('[')) return 'items';
350
+ // Handle defaults: a = 1 -> a
351
+ return p.split('=')[0].trim();
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Infer a TypeScript-style type for a parameter from context.
357
+ * @param {string} paramName
358
+ * @param {string[]} context
359
+ * @returns {string}
360
+ */
361
+ function inferParamType(paramName, context) {
362
+ const ctx = context.join(' ').toLowerCase();
363
+
364
+ if (paramName === 'options' || paramName === 'config') return 'Object';
365
+ if (paramName === 'items' || paramName === 'list') return 'Array';
366
+ if (ctx.includes('string') || ctx.includes('name') || ctx.includes('path')) return 'string';
367
+ if (ctx.includes('number') || ctx.includes('count') || ctx.includes('index')) return 'number';
368
+ if (ctx.includes('boolean') || ctx.includes('flag') || ctx.includes('enabled')) return 'boolean';
369
+ if (ctx.includes('callback') || ctx.includes('handler')) return 'Function';
370
+ if (ctx.includes('promise') || ctx.includes('async')) return 'Promise';
371
+ if (ctx.includes('array') || ctx.includes('list')) return 'Array';
372
+ if (ctx.includes('message')) return 'Message';
373
+ if (ctx.includes('request')) return 'Request';
374
+ if (ctx.includes('response')) return 'Response';
375
+
376
+ return '*';
377
+ }
378
+
379
+ /**
380
+ * Infer what a generator yields.
381
+ * @param {string} declaration
382
+ * @param {string[]} context
383
+ * @returns {string}
384
+ */
385
+ function inferYieldType(declaration, context) {
386
+ const ctx = context.join(' ');
387
+ if (ctx.includes('stream_event') || ctx.includes('StreamEvent')) return 'StreamEvent';
388
+ if (ctx.includes('message') || ctx.includes('Message')) return 'Message';
389
+ if (ctx.includes('chunk') || ctx.includes('Chunk')) return 'Chunk';
390
+ return 'Object';
391
+ }
392
+
393
+ /**
394
+ * Infer the return type of a function.
395
+ * @param {string} declaration
396
+ * @param {string[]} context
397
+ * @param {boolean} isAsync
398
+ * @returns {string|null}
399
+ */
400
+ function inferReturnType(declaration, context, isAsync) {
401
+ const ctx = context.join(' ');
402
+
403
+ let baseType = null;
404
+ if (ctx.includes('boolean') || ctx.includes('true') || ctx.includes('false')) {
405
+ baseType = 'boolean';
406
+ } else if (ctx.includes('string') || ctx.includes('"')) {
407
+ baseType = 'string';
408
+ } else if (ctx.includes('number') || ctx.includes('.length')) {
409
+ baseType = 'number';
410
+ } else if (ctx.includes('array') || ctx.includes('[]')) {
411
+ baseType = 'Array';
412
+ }
413
+
414
+ if (isAsync && baseType) return `Promise<${baseType}>`;
415
+ if (isAsync) return 'Promise<*>';
416
+ return baseType;
417
+ }
418
+
419
+ /**
420
+ * Capitalize the first letter of a string.
421
+ * @param {string} s
422
+ * @returns {string}
423
+ */
424
+ function capitalizeFirst(s) {
425
+ return s.charAt(0).toUpperCase() + s.slice(1);
426
+ }
427
+
428
+ module.exports = {
429
+ improveReadability,
430
+ convertMinifiedBooleans,
431
+ convertVoidZero,
432
+ convertToOptionalChaining,
433
+ expandCommaExprs,
434
+ addBlankLines,
435
+ generateJSDoc,
436
+ extractParams,
437
+ inferPurpose,
438
+ };