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,400 @@
1
+ /**
2
+ * name-predictor.js - Pattern-based name prediction for minified identifiers.
3
+ *
4
+ * Uses the 210+ training patterns from claude-code-patterns.json to infer
5
+ * meaningful names based on context strings, property accesses, and
6
+ * structural patterns found near each identifier.
7
+ *
8
+ * Falls back to structural heuristics when no pattern matches.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ /** Cached patterns array — loaded once. */
17
+ let _cachedPatterns = null;
18
+
19
+ /**
20
+ * Load patterns from the JSON training data.
21
+ * @param {string} [patternPath] - override path to patterns JSON
22
+ * @returns {Array<{context_strings: string[], property_names: string[], inferred_name: string, module_hint: string, confidence: number}>}
23
+ */
24
+ function loadPatterns(patternPath) {
25
+ if (_cachedPatterns && !patternPath) return _cachedPatterns;
26
+
27
+ const defaultPath = path.resolve(
28
+ __dirname,
29
+ '../../../../../crates/ruvector-decompiler/data/claude-code-patterns.json',
30
+ );
31
+ const resolved = patternPath || defaultPath;
32
+
33
+ try {
34
+ const raw = fs.readFileSync(resolved, 'utf-8');
35
+ _cachedPatterns = JSON.parse(raw);
36
+ return _cachedPatterns;
37
+ } catch {
38
+ // Pattern file not found — return empty, rely on structural rules
39
+ _cachedPatterns = [];
40
+ return _cachedPatterns;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Structural rename rules — deterministic, no model needed.
46
+ * Ordered by specificity (most specific first).
47
+ */
48
+ const STRUCTURAL_RULES = [
49
+ // Async generators
50
+ {
51
+ test: (decl) => /async\s+function\s*\*/.test(decl),
52
+ nameFrom: (decl, ctx) => {
53
+ if (ctx.some((s) => s.includes('agent')) && ctx.some((s) => s.includes('loop'))) return 'agentLoop';
54
+ if (ctx.some((s) => s.includes('stream'))) return 'streamGenerator';
55
+ return 'asyncGenerator';
56
+ },
57
+ confidence: 0.7,
58
+ },
59
+ // Error subclasses
60
+ {
61
+ test: (decl) => /class\s+\w+\s+extends\s+Error/.test(decl),
62
+ nameFrom: (decl) => {
63
+ const match = decl.match(/class\s+(\w+)\s+extends\s+(\w*Error)/);
64
+ if (match && !isMinifiedLike(match[1])) return match[1];
65
+ return 'CustomError';
66
+ },
67
+ confidence: 0.85,
68
+ },
69
+ // Class extending another class
70
+ {
71
+ test: (decl) => /class\s+\w+\s+extends\s+\w+/.test(decl),
72
+ nameFrom: (decl) => {
73
+ const match = decl.match(/class\s+(\w+)\s+extends\s+(\w+)/);
74
+ if (match && !isMinifiedLike(match[1])) return match[1];
75
+ if (match) return `${match[2]}Subclass`;
76
+ return null;
77
+ },
78
+ confidence: 0.75,
79
+ },
80
+ // Regular class
81
+ {
82
+ test: (decl) => /class\s+\w+/.test(decl),
83
+ nameFrom: (decl) => {
84
+ const match = decl.match(/class\s+(\w+)/);
85
+ if (match && !isMinifiedLike(match[1])) return match[1];
86
+ return null;
87
+ },
88
+ confidence: 0.7,
89
+ },
90
+ // Export default function
91
+ {
92
+ test: (decl) => /export\s+default\s+function/.test(decl),
93
+ nameFrom: () => 'defaultExport',
94
+ confidence: 0.5,
95
+ },
96
+ // Named function
97
+ {
98
+ test: (decl) => /function\s+([a-zA-Z_$]\w+)/.test(decl),
99
+ nameFrom: (decl) => {
100
+ const match = decl.match(/function\s+([a-zA-Z_$]\w+)/);
101
+ if (match && !isMinifiedLike(match[1])) return match[1];
102
+ return null;
103
+ },
104
+ confidence: 0.8,
105
+ },
106
+ ];
107
+
108
+ /**
109
+ * Parameter naming rules based on usage context.
110
+ */
111
+ const PARAM_RULES = [
112
+ { context: ['.messages', 'systemPrompt', 'canUseTool'], name: 'params', type: 'AgentLoopParams' },
113
+ { context: ['.method', '.url', '.headers'], name: 'request', type: 'Request' },
114
+ { context: ['.status', '.json', '.send'], name: 'response', type: 'Response' },
115
+ { context: ['.next', '.done', '.value'], name: 'iterator', type: 'Iterator' },
116
+ { context: ['.emit', '.on', '.removeListener'], name: 'emitter', type: 'EventEmitter' },
117
+ { context: ['.pipe', '.write', '.end'], name: 'stream', type: 'Stream' },
118
+ { context: ['.query', '.params', '.body'], name: 'req', type: 'Request' },
119
+ { context: ['.resolve', '.reject'], name: 'promise', type: 'Promise' },
120
+ { context: ['.name', '.version', '.description'], name: 'packageInfo', type: 'PackageInfo' },
121
+ { context: ['.key', '.value', '.ttl'], name: 'cacheEntry', type: 'CacheEntry' },
122
+ { context: ['.token', '.user', '.role'], name: 'session', type: 'Session' },
123
+ { context: ['.width', '.height', '.x', '.y'], name: 'rect', type: 'Rect' },
124
+ { context: ['.type', '.data', '.target'], name: 'event', type: 'Event' },
125
+ { context: ['.path', '.content', '.encoding'], name: 'file', type: 'FileInfo' },
126
+ { context: ['.host', '.port', '.protocol'], name: 'connectionInfo', type: 'ConnectionInfo' },
127
+ ];
128
+
129
+ /**
130
+ * Check if a name looks minified (short, no semantic meaning).
131
+ * @param {string} name
132
+ * @returns {boolean}
133
+ */
134
+ function isMinifiedLike(name) {
135
+ if (!name) return true;
136
+ return /^[a-zA-Z][a-zA-Z0-9$]{0,2}$/.test(name);
137
+ }
138
+
139
+ /**
140
+ * Score how well a pattern matches the given context.
141
+ *
142
+ * @param {object} pattern - from claude-code-patterns.json
143
+ * @param {string[]} contextStrings - strings found near the identifier
144
+ * @param {string[]} propertyNames - property accesses found near the identifier
145
+ * @returns {number} 0-1 match score
146
+ */
147
+ function scorePatternMatch(pattern, contextStrings, propertyNames) {
148
+ let score = 0;
149
+ let totalChecks = 0;
150
+
151
+ // Check context string matches
152
+ if (pattern.context_strings && pattern.context_strings.length > 0) {
153
+ for (const ctx of pattern.context_strings) {
154
+ totalChecks++;
155
+ if (contextStrings.some((s) => s.includes(ctx) || ctx.includes(s))) {
156
+ score++;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Check property name matches
162
+ if (pattern.property_names && pattern.property_names.length > 0) {
163
+ for (const prop of pattern.property_names) {
164
+ totalChecks++;
165
+ const propAccess = `.${prop}`;
166
+ if (propertyNames.some((p) => p === propAccess || p === prop)) {
167
+ score++;
168
+ }
169
+ }
170
+ }
171
+
172
+ if (totalChecks === 0) return 0;
173
+ return score / totalChecks;
174
+ }
175
+
176
+ /**
177
+ * Predict a meaningful name for a minified identifier.
178
+ *
179
+ * @param {string} minifiedName - the original short name (e.g. "s$")
180
+ * @param {string[]} contextStrings - strings/properties near the identifier
181
+ * @param {object} [options]
182
+ * @param {string} [options.declaration] - the declaration statement
183
+ * @param {string} [options.patternPath] - path to patterns JSON
184
+ * @param {number} [options.minConfidence=0.3] - minimum confidence to accept
185
+ * @returns {{name: string, confidence: number, source: string}|null}
186
+ */
187
+ function predictName(minifiedName, contextStrings, options = {}) {
188
+ const { declaration = '', patternPath, minConfidence = 0.3 } = options;
189
+
190
+ // Separate context strings from property accesses
191
+ const props = contextStrings.filter((s) => s.startsWith('.'));
192
+ const strings = contextStrings.filter((s) => !s.startsWith('.'));
193
+
194
+ // 1. Try direct-assignment analysis (highest precision)
195
+ // If X = Y.propertyName, then X should be named "propertyName"
196
+ const directName = inferFromDirectAssignment(minifiedName, declaration);
197
+ if (directName) {
198
+ return { name: directName.name, confidence: directName.confidence, source: 'direct-assign' };
199
+ }
200
+
201
+ // 2. Try structural rules (high specificity)
202
+ for (const rule of STRUCTURAL_RULES) {
203
+ if (rule.test(declaration)) {
204
+ const name = rule.nameFrom(declaration, contextStrings);
205
+ if (name && rule.confidence >= minConfidence) {
206
+ return { name, confidence: rule.confidence, source: 'structural' };
207
+ }
208
+ }
209
+ }
210
+
211
+ // 3. Try pattern matching against training data
212
+ const patterns = loadPatterns(patternPath);
213
+ let bestMatch = null;
214
+ let bestScore = 0;
215
+
216
+ for (const pattern of patterns) {
217
+ const matchScore = scorePatternMatch(pattern, strings, props);
218
+ const adjustedScore = matchScore * pattern.confidence;
219
+
220
+ if (adjustedScore > bestScore && adjustedScore >= minConfidence) {
221
+ bestScore = adjustedScore;
222
+ bestMatch = pattern;
223
+ }
224
+ }
225
+
226
+ if (bestMatch) {
227
+ return {
228
+ name: toCamelCase(bestMatch.inferred_name),
229
+ confidence: bestScore,
230
+ source: 'pattern',
231
+ };
232
+ }
233
+
234
+ // 3. Try parameter naming rules
235
+ for (const rule of PARAM_RULES) {
236
+ const matches = rule.context.filter((c) =>
237
+ contextStrings.some((s) => s.includes(c)),
238
+ );
239
+ if (matches.length >= 2) {
240
+ return {
241
+ name: rule.name,
242
+ confidence: 0.6,
243
+ source: 'param-rule',
244
+ type: rule.type,
245
+ };
246
+ }
247
+ }
248
+
249
+ // 4. Heuristic fallbacks based on usage patterns
250
+ const heuristic = heuristicName(minifiedName, contextStrings, declaration);
251
+ if (heuristic && heuristic.confidence >= minConfidence) {
252
+ return heuristic;
253
+ }
254
+
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Infer a name from direct assignment patterns.
260
+ * let B = A.messages -> B should be "messages"
261
+ * let Z = await Y2(B, G, ...) -> Z might be "result" or inferred from Y2
262
+ * var s$ = async function*(...) -> handled by structural rules
263
+ *
264
+ * @param {string} minifiedName
265
+ * @param {string} declaration
266
+ * @returns {{name: string, confidence: number}|null}
267
+ */
268
+ function inferFromDirectAssignment(minifiedName, declaration) {
269
+ if (!declaration) return null;
270
+ const escaped = minifiedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
271
+
272
+ // Pattern: X = Y.propertyName (property extraction)
273
+ const propMatch = declaration.match(
274
+ new RegExp(`${escaped}\\s*=\\s*\\w+\\.(\\w{2,30})`)
275
+ );
276
+ if (propMatch && propMatch[1].length > 2) {
277
+ return { name: propMatch[1], confidence: 0.85 };
278
+ }
279
+
280
+ // Pattern: X = await Y(...) (function call result)
281
+ const awaitCallMatch = declaration.match(
282
+ new RegExp(`${escaped}\\s*=\\s*await\\s+(\\w+)\\(`)
283
+ );
284
+ if (awaitCallMatch) {
285
+ const fnName = awaitCallMatch[1];
286
+ if (!isMinifiedLike(fnName)) {
287
+ // Infer from function name: createApiRequest -> apiRequest
288
+ const resultName = fnName.replace(/^(create|get|fetch|make|build|load)/, '').replace(/^./, (c) => c.toLowerCase());
289
+ return { name: resultName || 'result', confidence: 0.7 };
290
+ }
291
+ return { name: 'result', confidence: 0.5 };
292
+ }
293
+
294
+ // Pattern: for await (let J of Z) -> J is an item from the iterable
295
+ const forOfMatch = declaration.match(
296
+ new RegExp(`for\\s+await\\s*\\(\\s*(?:let|const|var)\\s+${escaped}\\s+of\\s+(\\w+)`)
297
+ );
298
+ if (forOfMatch) {
299
+ return { name: 'item', confidence: 0.5 };
300
+ }
301
+
302
+ // Pattern: function parameter (first param of function*)
303
+ const funcParamMatch = declaration.match(
304
+ new RegExp(`function\\s*\\*?\\s*\\w*\\s*\\(\\s*${escaped}(?:\\s*,|\\s*\\))`)
305
+ );
306
+ if (funcParamMatch) {
307
+ // First parameter of a function -> "params" or "options"
308
+ return { name: 'params', confidence: 0.5 };
309
+ }
310
+
311
+ return null;
312
+ }
313
+
314
+ /**
315
+ * Heuristic name inference from usage patterns.
316
+ *
317
+ * @param {string} minifiedName
318
+ * @param {string[]} contextStrings
319
+ * @param {string} declaration
320
+ * @returns {{name: string, confidence: number, source: string}|null}
321
+ */
322
+ function heuristicName(minifiedName, contextStrings, declaration) {
323
+ const ctx = contextStrings.join(' ').toLowerCase();
324
+
325
+ // Callback / handler patterns
326
+ if (ctx.includes('callback') || ctx.includes('handler') || ctx.includes('listener')) {
327
+ return { name: 'handler', confidence: 0.4, source: 'heuristic' };
328
+ }
329
+ // Error variable
330
+ if (/catch\s*\(\s*$/.test(declaration) || ctx.includes('error') && ctx.includes('catch')) {
331
+ return { name: 'error', confidence: 0.5, source: 'heuristic' };
332
+ }
333
+ // Iterator / loop variable used with .length or [i]
334
+ if (ctx.includes('.length') && ctx.includes('for')) {
335
+ return { name: 'items', confidence: 0.4, source: 'heuristic' };
336
+ }
337
+ // Result of await
338
+ if (declaration.includes('await')) {
339
+ if (ctx.includes('fetch') || ctx.includes('request')) {
340
+ return { name: 'response', confidence: 0.5, source: 'heuristic' };
341
+ }
342
+ return { name: 'result', confidence: 0.35, source: 'heuristic' };
343
+ }
344
+ // Boolean-looking (used in conditions)
345
+ if (ctx.includes('if') && (ctx.includes('===true') || ctx.includes('===false') || ctx.includes('!!'))) {
346
+ return { name: 'isEnabled', confidence: 0.35, source: 'heuristic' };
347
+ }
348
+
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * Convert a PascalCase or kebab-case name to camelCase.
354
+ * @param {string} name
355
+ * @returns {string}
356
+ */
357
+ function toCamelCase(name) {
358
+ if (!name) return name;
359
+ // If already camelCase, return as-is
360
+ if (/^[a-z]/.test(name) && !name.includes('-') && !name.includes('_')) return name;
361
+ // PascalCase -> camelCase
362
+ if (/^[A-Z]/.test(name) && !name.includes('-') && !name.includes('_')) {
363
+ return name[0].toLowerCase() + name.slice(1);
364
+ }
365
+ // kebab-case or snake_case -> camelCase
366
+ return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase());
367
+ }
368
+
369
+ /**
370
+ * Infer a parameter name from its position and usage.
371
+ * @param {number} index - parameter position (0-based)
372
+ * @param {string[]} contextStrings
373
+ * @returns {string}
374
+ */
375
+ function inferParamName(index, contextStrings) {
376
+ // Try param rules
377
+ for (const rule of PARAM_RULES) {
378
+ const matches = rule.context.filter((c) =>
379
+ contextStrings.some((s) => s.includes(c)),
380
+ );
381
+ if (matches.length >= 1) {
382
+ return rule.name;
383
+ }
384
+ }
385
+
386
+ // Generic fallback
387
+ const fallbacks = ['param', 'value', 'data', 'input', 'arg', 'item'];
388
+ return fallbacks[index] || `param${index}`;
389
+ }
390
+
391
+ module.exports = {
392
+ predictName,
393
+ loadPatterns,
394
+ scorePatternMatch,
395
+ inferParamName,
396
+ toCamelCase,
397
+ isMinifiedLike,
398
+ STRUCTURAL_RULES,
399
+ PARAM_RULES,
400
+ };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * npm-fetch.js - Fetch package info and files from the npm registry.
3
+ *
4
+ * Uses the built-in Node 18+ fetch API. Retrieves package metadata from
5
+ * registry.npmjs.org and file contents from unpkg.com / jsdelivr.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const REGISTRY_BASE = 'https://registry.npmjs.org';
11
+ const JSDELIVR_BASE = 'https://data.jsdelivr.com/v1/package/npm';
12
+ const UNPKG_BASE = 'https://unpkg.com';
13
+
14
+ /**
15
+ * Fetch package metadata from the npm registry.
16
+ * @param {string} packageName - npm package name (e.g. 'express', '@anthropic-ai/claude-code')
17
+ * @returns {Promise<{name: string, description: string, versions: string[], latest: string, distTags: object, packageJson: object}>}
18
+ */
19
+ async function fetchPackageInfo(packageName) {
20
+ const url = `${REGISTRY_BASE}/${encodeURIComponent(packageName).replace('%40', '@')}`;
21
+ const resp = await fetch(url, {
22
+ headers: { Accept: 'application/json' },
23
+ });
24
+
25
+ if (!resp.ok) {
26
+ throw new Error(`Package "${packageName}" not found (HTTP ${resp.status})`);
27
+ }
28
+
29
+ const data = await resp.json();
30
+ const versions = Object.keys(data.versions || {}).reverse();
31
+ const distTags = data['dist-tags'] || {};
32
+ const latest = distTags.latest || versions[0] || '';
33
+
34
+ return {
35
+ name: data.name,
36
+ description: data.description || '',
37
+ versions,
38
+ latest,
39
+ distTags,
40
+ packageJson: data.versions?.[latest] || {},
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Fetch the file list for a specific package version via jsDelivr.
46
+ * @param {string} packageName
47
+ * @param {string} version
48
+ * @returns {Promise<Array<{name: string, hash: string, size: number}>>}
49
+ */
50
+ async function fetchPackageFileList(packageName, version) {
51
+ const encodedName = encodeURIComponent(packageName).replace('%40', '@');
52
+ const url = `${JSDELIVR_BASE}/${encodedName}@${version}/flat`;
53
+ const resp = await fetch(url);
54
+ if (!resp.ok) {
55
+ throw new Error(`Could not list files for ${packageName}@${version}`);
56
+ }
57
+ const data = await resp.json();
58
+ return (data.files || []);
59
+ }
60
+
61
+ /**
62
+ * Fetch the content of a single file from unpkg.
63
+ * @param {string} packageName
64
+ * @param {string} version
65
+ * @param {string} filePath - e.g. '/dist/index.js'
66
+ * @returns {Promise<string>}
67
+ */
68
+ async function fetchFileContent(packageName, version, filePath) {
69
+ const cleanPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
70
+ const url = `${UNPKG_BASE}/${packageName}@${version}/${cleanPath}`;
71
+ const resp = await fetch(url, { redirect: 'follow' });
72
+ if (!resp.ok) {
73
+ throw new Error(`Could not fetch ${cleanPath} from ${packageName}@${version} (HTTP ${resp.status})`);
74
+ }
75
+ return resp.text();
76
+ }
77
+
78
+ /**
79
+ * Find the main JS bundle file from a file list.
80
+ * Checks common locations and falls back to the largest .js file.
81
+ * @param {Array<{name: string, size: number}>} files
82
+ * @param {object} [packageJson] - optional package.json to check main/module/browser fields
83
+ * @returns {string|null}
84
+ */
85
+ function findMainBundle(files, packageJson) {
86
+ // Check package.json fields first
87
+ if (packageJson) {
88
+ const fields = ['browser', 'module', 'main'];
89
+ for (const field of fields) {
90
+ if (typeof packageJson[field] === 'string') {
91
+ const normalized = packageJson[field].startsWith('/')
92
+ ? packageJson[field]
93
+ : '/' + packageJson[field];
94
+ if (files.some((f) => f.name === normalized)) {
95
+ return normalized;
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // Well-known candidate paths
102
+ const candidates = [
103
+ '/dist/cli.js',
104
+ '/dist/index.js',
105
+ '/dist/main.js',
106
+ '/dist/bundle.js',
107
+ '/lib/index.js',
108
+ '/lib/cli.js',
109
+ '/index.js',
110
+ '/cli.js',
111
+ ];
112
+
113
+ for (const candidate of candidates) {
114
+ if (files.some((f) => f.name === candidate)) {
115
+ return candidate;
116
+ }
117
+ }
118
+
119
+ // Fallback: largest JS file (prefer non-minified)
120
+ const jsFiles = files
121
+ .filter((f) => f.name.endsWith('.js') && !f.name.endsWith('.min.js'))
122
+ .sort((a, b) => b.size - a.size);
123
+
124
+ return jsFiles.length > 0 ? jsFiles[0].name : null;
125
+ }
126
+
127
+ /**
128
+ * Parse a target string into its components.
129
+ * Handles: 'express', 'express@4.18.2', '@scope/pkg@1.0.0',
130
+ * './local.js', 'https://...'.
131
+ * @param {string} target
132
+ * @returns {{type: 'npm'|'file'|'url', name?: string, version?: string, path?: string, url?: string}}
133
+ */
134
+ function parseTarget(target) {
135
+ if (target.startsWith('http://') || target.startsWith('https://')) {
136
+ return { type: 'url', url: target };
137
+ }
138
+
139
+ if (target.startsWith('.') || target.startsWith('/')) {
140
+ return { type: 'file', path: target };
141
+ }
142
+
143
+ // npm package: @scope/name@version or name@version
144
+ let name = target;
145
+ let version = undefined;
146
+ if (target.startsWith('@')) {
147
+ // scoped: @scope/name@version
148
+ const afterScope = target.indexOf('/', 1);
149
+ if (afterScope > 0) {
150
+ const atIdx = target.indexOf('@', afterScope + 1);
151
+ if (atIdx > 0) {
152
+ name = target.slice(0, atIdx);
153
+ version = target.slice(atIdx + 1);
154
+ }
155
+ }
156
+ } else {
157
+ const atIdx = target.indexOf('@');
158
+ if (atIdx > 0) {
159
+ name = target.slice(0, atIdx);
160
+ version = target.slice(atIdx + 1);
161
+ }
162
+ }
163
+
164
+ return { type: 'npm', name, version };
165
+ }
166
+
167
+ module.exports = {
168
+ fetchPackageInfo,
169
+ fetchPackageFileList,
170
+ fetchFileContent,
171
+ findMainBundle,
172
+ parseTarget,
173
+ REGISTRY_BASE,
174
+ UNPKG_BASE,
175
+ JSDELIVR_BASE,
176
+ };