project-graph-mcp 1.3.0 → 1.5.0
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/AGENT_ROLE.md +87 -30
- package/AGENT_ROLE_MINIMAL.md +23 -8
- package/README.md +100 -14
- package/package.json +1 -1
- package/src/.project-graph-cache.json +1 -0
- package/src/ai-context.js +113 -0
- package/src/analysis-cache.js +155 -0
- package/src/cli-handlers.js +131 -0
- package/src/cli.js +14 -2
- package/src/compact.js +207 -0
- package/src/complexity.js +21 -7
- package/src/compress.js +319 -0
- package/src/ctx-to-jsdoc.js +514 -0
- package/src/custom-rules.js +1 -0
- package/src/doc-dialect.js +716 -0
- package/src/full-analysis.js +307 -11
- package/src/instructions.js +3 -105
- package/src/jsdoc-checker.js +351 -0
- package/src/jsdoc-generator.js +0 -11
- package/src/large-files.js +1 -0
- package/src/mcp-server.js +208 -1
- package/src/mode-config.js +127 -0
- package/src/outdated-patterns.js +1 -0
- package/src/parser.js +223 -13
- package/src/similar-functions.js +1 -0
- package/src/test-annotations.js +203 -181
- package/src/tool-defs.js +270 -2
- package/src/tools.js +1 -1
- package/src/type-checker.js +188 -0
- package/src/undocumented.js +11 -12
- package/src/workspace.js +1 -1
- package/vendor/terser.mjs +49 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact Code Mode Configuration
|
|
3
|
+
*
|
|
4
|
+
* Manages project-level mode selection for the compact code architecture.
|
|
5
|
+
* Reads/writes `.context/config.json` to configure how agents interact with code.
|
|
6
|
+
*
|
|
7
|
+
* Modes:
|
|
8
|
+
* 1 = Native Compact: code stored minified, agent edits directly
|
|
9
|
+
* 2 = Full Storage: code stored formatted, agent reads compressed view, edits via edit_compressed
|
|
10
|
+
* 3 = Future (IDE): compact storage with IDE virtual display (reserved)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
|
|
16
|
+
const CONFIG_FILE = '.context/config.json';
|
|
17
|
+
|
|
18
|
+
/** Default configuration */
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
mode: 2,
|
|
21
|
+
beautify: true,
|
|
22
|
+
autoValidate: false,
|
|
23
|
+
stripJSDoc: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read project mode configuration from .context/config.json
|
|
28
|
+
* Returns defaults if file doesn't exist.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} projectDir - Project root directory
|
|
31
|
+
* @returns {{ mode: number, beautify: boolean, autoValidate: boolean, stripJSDoc: boolean }}
|
|
32
|
+
*/
|
|
33
|
+
export function getConfig(projectDir) {
|
|
34
|
+
const configPath = join(projectDir, CONFIG_FILE);
|
|
35
|
+
|
|
36
|
+
if (!existsSync(configPath)) {
|
|
37
|
+
return { ...DEFAULTS };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return { ...DEFAULTS, ...parsed };
|
|
44
|
+
} catch {
|
|
45
|
+
return { ...DEFAULTS };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write project mode configuration to .context/config.json
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectDir - Project root directory
|
|
53
|
+
* @param {Object} config - Configuration to save (merged with existing)
|
|
54
|
+
* @returns {{ saved: boolean, path: string, config: Object }}
|
|
55
|
+
*/
|
|
56
|
+
export function setConfig(projectDir, config) {
|
|
57
|
+
const configPath = join(projectDir, CONFIG_FILE);
|
|
58
|
+
const dir = dirname(configPath);
|
|
59
|
+
|
|
60
|
+
if (!existsSync(dir)) {
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Merge with existing
|
|
65
|
+
const existing = getConfig(projectDir);
|
|
66
|
+
const merged = { ...existing, ...config };
|
|
67
|
+
|
|
68
|
+
// Validate mode
|
|
69
|
+
if (![1, 2, 3].includes(merged.mode)) {
|
|
70
|
+
throw new Error(`Invalid mode: ${merged.mode}. Valid: 1 (compact), 2 (full), 3 (IDE)`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
saved: true,
|
|
77
|
+
path: configPath,
|
|
78
|
+
config: merged,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get human-readable description of current mode
|
|
84
|
+
* @param {number} mode
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function getModeDescription(mode) {
|
|
88
|
+
switch (mode) {
|
|
89
|
+
case 1: return 'Native Compact — code stored minified, agent edits directly';
|
|
90
|
+
case 2: return 'Full Storage — code stored formatted, agent uses get_compressed_file + edit_compressed';
|
|
91
|
+
case 3: return 'IDE Virtual — compact storage with IDE virtual display (future)';
|
|
92
|
+
default: return `Unknown mode: ${mode}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get recommended workflow for current mode
|
|
98
|
+
* @param {number} mode
|
|
99
|
+
* @returns {{ read: string, edit: string, docs: string, validate: string }}
|
|
100
|
+
*/
|
|
101
|
+
export function getModeWorkflow(mode) {
|
|
102
|
+
switch (mode) {
|
|
103
|
+
case 1:
|
|
104
|
+
return {
|
|
105
|
+
read: 'Read .js files directly (already compact)',
|
|
106
|
+
edit: 'Edit .js files directly',
|
|
107
|
+
docs: 'Read .ctx files for types and descriptions',
|
|
108
|
+
validate: 'Run validate-ctx to check .ctx ↔ AST consistency',
|
|
109
|
+
};
|
|
110
|
+
case 2:
|
|
111
|
+
return {
|
|
112
|
+
read: 'Use get_compressed_file for token-efficient reading',
|
|
113
|
+
edit: 'Use edit_compressed(path, symbol, code) for AST-safe editing',
|
|
114
|
+
docs: 'Read .ctx files for types and descriptions',
|
|
115
|
+
validate: 'Run validate-ctx to check .ctx ↔ AST consistency',
|
|
116
|
+
};
|
|
117
|
+
case 3:
|
|
118
|
+
return {
|
|
119
|
+
read: 'IDE renders full view from compact storage',
|
|
120
|
+
edit: 'IDE handles bidirectional mapping',
|
|
121
|
+
docs: 'Managed by IDE plugin',
|
|
122
|
+
validate: 'Automatic via IDE integration',
|
|
123
|
+
};
|
|
124
|
+
default:
|
|
125
|
+
return { read: 'N/A', edit: 'N/A', docs: 'N/A', validate: 'N/A' };
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/outdated-patterns.js
CHANGED
|
@@ -152,6 +152,7 @@ function findJSFiles(dir, rootDir = dir) {
|
|
|
152
152
|
/**
|
|
153
153
|
* Analyze file for outdated patterns
|
|
154
154
|
* @param {string} filePath
|
|
155
|
+
* @param {string} rootDir - Root directory for relative path calculation
|
|
155
156
|
* @returns {PatternMatch[]}
|
|
156
157
|
*/
|
|
157
158
|
function analyzeFilePatterns(filePath, rootDir) {
|
package/src/parser.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracts classes, functions, methods, properties, imports, calls, and SQL queries
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
6
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
7
7
|
import { join, relative, resolve } from 'path';
|
|
8
8
|
import { parse } from '../vendor/acorn.mjs';
|
|
9
9
|
import * as walk from '../vendor/walk.mjs';
|
|
@@ -64,12 +64,15 @@ export async function parseFile(code, filename) {
|
|
|
64
64
|
exports: [],
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// Collect JSDoc comments for type extraction
|
|
68
|
+
const comments = [];
|
|
67
69
|
let ast;
|
|
68
70
|
try {
|
|
69
71
|
ast = parse(code, {
|
|
70
72
|
ecmaVersion: 'latest',
|
|
71
73
|
sourceType: 'module',
|
|
72
74
|
locations: true,
|
|
75
|
+
onComment: comments,
|
|
73
76
|
});
|
|
74
77
|
} catch (e) {
|
|
75
78
|
// If parsing fails, return empty result
|
|
@@ -77,6 +80,9 @@ export async function parseFile(code, filename) {
|
|
|
77
80
|
return result;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
// Build JSDoc type map: endLine → { params: [{name, type}], returns: string }
|
|
84
|
+
const jsdocMap = buildJSDocTypeMap(comments, code);
|
|
85
|
+
|
|
80
86
|
// Track exported names
|
|
81
87
|
const exportedNames = new Set();
|
|
82
88
|
|
|
@@ -158,9 +164,24 @@ export async function parseFile(code, filename) {
|
|
|
158
164
|
// Standalone function declarations
|
|
159
165
|
FunctionDeclaration(node) {
|
|
160
166
|
if (node.id) {
|
|
167
|
+
const rawParams = node.params.map(p => {
|
|
168
|
+
if (p.type === 'Identifier') return p.name;
|
|
169
|
+
if (p.type === 'AssignmentPattern' && p.left.type === 'Identifier') return p.left.name + '=';
|
|
170
|
+
if (p.type === 'RestElement' && p.argument.type === 'Identifier') return '...' + p.argument.name;
|
|
171
|
+
if (p.type === 'ObjectPattern') return 'options';
|
|
172
|
+
return '?';
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Enrich params with JSDoc types
|
|
176
|
+
const jsdoc = findJSDocForNode(jsdocMap, node.loc.start.line);
|
|
177
|
+
const typedParams = enrichParamsWithTypes(rawParams, jsdoc);
|
|
178
|
+
|
|
161
179
|
const funcInfo = {
|
|
162
180
|
name: node.id.name,
|
|
163
181
|
exported: false, // Will be updated later
|
|
182
|
+
params: typedParams,
|
|
183
|
+
async: node.async || false,
|
|
184
|
+
returns: jsdoc?.returns || null,
|
|
164
185
|
calls: [],
|
|
165
186
|
dbReads: [],
|
|
166
187
|
dbWrites: [],
|
|
@@ -338,12 +359,53 @@ function templateToString(tplNode) {
|
|
|
338
359
|
return result;
|
|
339
360
|
}
|
|
340
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Discover sub-projects in a monorepo directory structure
|
|
364
|
+
* @param {string} rootDir
|
|
365
|
+
* @returns {Array<{name: string, path: string, absolutePath: string}>}
|
|
366
|
+
*/
|
|
367
|
+
export function discoverSubProjects(rootDir) {
|
|
368
|
+
const resolvedRoot = resolve(rootDir);
|
|
369
|
+
const subProjects = [];
|
|
370
|
+
|
|
371
|
+
// Known monorepo directory conventions
|
|
372
|
+
const MONO_DIRS = ['packages', 'apps', 'services', 'modules', 'libs', 'plugins'];
|
|
373
|
+
|
|
374
|
+
for (const monoDir of MONO_DIRS) {
|
|
375
|
+
const monoPath = join(resolvedRoot, monoDir);
|
|
376
|
+
if (!existsSync(monoPath)) continue;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
for (const entry of readdirSync(monoPath)) {
|
|
380
|
+
const entryPath = join(monoPath, entry);
|
|
381
|
+
const pkgPath = join(entryPath, 'package.json');
|
|
382
|
+
if (statSync(entryPath).isDirectory() && existsSync(pkgPath)) {
|
|
383
|
+
try {
|
|
384
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
385
|
+
subProjects.push({
|
|
386
|
+
name: pkg.name || entry,
|
|
387
|
+
path: relative(resolvedRoot, entryPath),
|
|
388
|
+
absolutePath: entryPath,
|
|
389
|
+
});
|
|
390
|
+
} catch {
|
|
391
|
+
subProjects.push({ name: entry, path: relative(resolvedRoot, entryPath), absolutePath: entryPath });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch { /* dir not readable */ }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return subProjects;
|
|
399
|
+
}
|
|
400
|
+
|
|
341
401
|
/**
|
|
342
402
|
* Parse all JS files in a directory
|
|
343
403
|
* @param {string} dir
|
|
404
|
+
* @param {Object} [options={}]
|
|
405
|
+
* @param {boolean} [options.recursive=false]
|
|
344
406
|
* @returns {Promise<ParseResult>}
|
|
345
407
|
*/
|
|
346
|
-
export async function parseProject(dir) {
|
|
408
|
+
export async function parseProject(dir, options = {}) {
|
|
347
409
|
const result = {
|
|
348
410
|
files: [],
|
|
349
411
|
classes: [],
|
|
@@ -357,17 +419,48 @@ export async function parseProject(dir) {
|
|
|
357
419
|
const files = findJSFiles(dir);
|
|
358
420
|
|
|
359
421
|
for (const file of files) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
422
|
+
try {
|
|
423
|
+
const content = readFileSync(file, 'utf-8');
|
|
424
|
+
const relPath = relative(resolvedDir, file);
|
|
425
|
+
const parsed = await parseFileByExtension(content, relPath);
|
|
426
|
+
|
|
427
|
+
result.files.push(relPath);
|
|
428
|
+
result.classes.push(...parsed.classes);
|
|
429
|
+
result.functions.push(...parsed.functions);
|
|
430
|
+
result.imports.push(...parsed.imports);
|
|
431
|
+
result.exports.push(...parsed.exports);
|
|
432
|
+
if (parsed.tables?.length) {
|
|
433
|
+
result.tables.push(...parsed.tables);
|
|
434
|
+
}
|
|
435
|
+
} catch (e) {
|
|
436
|
+
// Ignore unreadable files
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Recursive monorepo support
|
|
441
|
+
if (options.recursive) {
|
|
442
|
+
const subs = discoverSubProjects(dir);
|
|
443
|
+
result.subProjects = [];
|
|
444
|
+
for (const sub of subs) {
|
|
445
|
+
try {
|
|
446
|
+
const subResult = await parseProject(sub.absolutePath);
|
|
447
|
+
// Prefix all file paths with sub-project path
|
|
448
|
+
for (const f of subResult.files) {
|
|
449
|
+
result.files.push(join(sub.path, f));
|
|
450
|
+
}
|
|
451
|
+
for (const c of subResult.classes) {
|
|
452
|
+
c.file = join(sub.path, c.file);
|
|
453
|
+
result.classes.push(c);
|
|
454
|
+
}
|
|
455
|
+
for (const fn of subResult.functions) {
|
|
456
|
+
fn.file = join(sub.path, fn.file);
|
|
457
|
+
result.functions.push(fn);
|
|
458
|
+
}
|
|
459
|
+
result.imports.push(...subResult.imports);
|
|
460
|
+
result.exports.push(...subResult.exports);
|
|
461
|
+
if (subResult.tables?.length) result.tables.push(...subResult.tables);
|
|
462
|
+
result.subProjects.push({ name: sub.name, path: sub.path, files: subResult.files.length });
|
|
463
|
+
} catch { /* sub-project parse failure is non-fatal */ }
|
|
371
464
|
}
|
|
372
465
|
}
|
|
373
466
|
|
|
@@ -450,3 +543,120 @@ export function findJSFiles(dir, rootDir = dir) {
|
|
|
450
543
|
|
|
451
544
|
return files;
|
|
452
545
|
}
|
|
546
|
+
|
|
547
|
+
// ============================
|
|
548
|
+
// JSDoc Type Extraction
|
|
549
|
+
// ============================
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Build a map of JSDoc comment end-lines to their extracted type info.
|
|
553
|
+
* @param {Array} comments - Acorn onComment array
|
|
554
|
+
* @param {string} code - Full source code
|
|
555
|
+
* @returns {Map<number, {params: Array<{name: string, type: string}>, returns: string|null}>}
|
|
556
|
+
*/
|
|
557
|
+
function buildJSDocTypeMap(comments, code) {
|
|
558
|
+
const map = new Map();
|
|
559
|
+
|
|
560
|
+
for (const comment of comments) {
|
|
561
|
+
// Only process JSDoc blocks (/** ... */)
|
|
562
|
+
if (comment.type !== 'Block' || !comment.value.startsWith('*')) continue;
|
|
563
|
+
|
|
564
|
+
const text = '/*' + comment.value + '*/';
|
|
565
|
+
const endLine = code.slice(0, comment.end).split('\n').length;
|
|
566
|
+
|
|
567
|
+
// Parse @param tags with balanced brace matching
|
|
568
|
+
const params = [];
|
|
569
|
+
const paramStartRegex = /@param\s+\{/g;
|
|
570
|
+
let paramStart;
|
|
571
|
+
while ((paramStart = paramStartRegex.exec(text)) !== null) {
|
|
572
|
+
// Find matching closing brace (balanced — handles {Array<{text: string}>})
|
|
573
|
+
let depth = 1;
|
|
574
|
+
let i = paramStart.index + paramStart[0].length;
|
|
575
|
+
while (i < text.length && depth > 0) {
|
|
576
|
+
if (text[i] === '{') depth++;
|
|
577
|
+
else if (text[i] === '}') depth--;
|
|
578
|
+
i++;
|
|
579
|
+
}
|
|
580
|
+
if (depth !== 0) continue;
|
|
581
|
+
const type = text.slice(paramStart.index + paramStart[0].length, i - 1);
|
|
582
|
+
// Extract param name after the closing brace
|
|
583
|
+
const afterType = text.slice(i);
|
|
584
|
+
const nameMatch = afterType.match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);
|
|
585
|
+
if (!nameMatch) continue;
|
|
586
|
+
let name = nameMatch[1];
|
|
587
|
+
// Strip [] from optional params: [opts] → opts
|
|
588
|
+
if (name.startsWith('[')) name = name.slice(1);
|
|
589
|
+
if (name.endsWith(']')) name = name.slice(0, -1);
|
|
590
|
+
// Skip dotted paths (options.x)
|
|
591
|
+
if (name.includes('.')) continue;
|
|
592
|
+
params.push({ name, type });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Parse @returns {Type}
|
|
596
|
+
let returns = null;
|
|
597
|
+
const returnsMatch = text.match(/@returns?\s+\{([^}]+)\}/);
|
|
598
|
+
if (returnsMatch) {
|
|
599
|
+
returns = returnsMatch[1];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (params.length > 0 || returns) {
|
|
603
|
+
map.set(endLine, { params, returns });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return map;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Find the JSDoc entry that applies to a function at the given line.
|
|
612
|
+
* JSDoc must end within 2 lines above the function declaration.
|
|
613
|
+
* @param {Map} jsdocMap
|
|
614
|
+
* @param {number} funcLine - Function start line
|
|
615
|
+
* @returns {{ params: Array<{name: string, type: string}>, returns: string|null }|null}
|
|
616
|
+
*/
|
|
617
|
+
function findJSDocForNode(jsdocMap, funcLine) {
|
|
618
|
+
// JSDoc can end 1 or 2 lines above (direct or with blank line)
|
|
619
|
+
for (let offset = 1; offset <= 3; offset++) {
|
|
620
|
+
const entry = jsdocMap.get(funcLine - offset);
|
|
621
|
+
if (entry) return entry;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Enrich AST-extracted param names with types from JSDoc.
|
|
628
|
+
* Input: ['filePath', 'options='] + jsdoc.params: [{name:'filePath', type:'string'}, {name:'options', type:'Object'}]
|
|
629
|
+
* Output: ['filePath:string', 'options:Object=']
|
|
630
|
+
* @param {string[]} rawParams
|
|
631
|
+
* @param {Object|null} jsdoc
|
|
632
|
+
* @returns {string[]}
|
|
633
|
+
*/
|
|
634
|
+
function enrichParamsWithTypes(rawParams, jsdoc) {
|
|
635
|
+
if (!jsdoc || jsdoc.params.length === 0) return rawParams;
|
|
636
|
+
|
|
637
|
+
// Build name→type lookup from JSDoc
|
|
638
|
+
const typeMap = new Map();
|
|
639
|
+
for (const p of jsdoc.params) {
|
|
640
|
+
typeMap.set(p.name, p.type);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return rawParams.map(param => {
|
|
644
|
+
// Parse: '...name', 'name=', 'name', 'options'
|
|
645
|
+
const isRest = param.startsWith('...');
|
|
646
|
+
const hasDefault = param.endsWith('=');
|
|
647
|
+
let cleanName = param;
|
|
648
|
+
if (isRest) cleanName = cleanName.slice(3);
|
|
649
|
+
if (hasDefault) cleanName = cleanName.slice(0, -1);
|
|
650
|
+
|
|
651
|
+
let type = typeMap.get(cleanName);
|
|
652
|
+
if (!type) return param; // No JSDoc type found
|
|
653
|
+
|
|
654
|
+
// Strip JSDoc rest indicator {...Type} — rest is already from AST
|
|
655
|
+
if (type.startsWith('...')) type = type.slice(3);
|
|
656
|
+
|
|
657
|
+
// Reconstruct: ...name:Type, name:Type=, name:Type
|
|
658
|
+
const prefix = isRest ? '...' : '';
|
|
659
|
+
const suffix = hasDefault ? '=' : '';
|
|
660
|
+
return `${prefix}${cleanName}:${type}${suffix}`;
|
|
661
|
+
});
|
|
662
|
+
}
|
package/src/similar-functions.js
CHANGED
|
@@ -63,6 +63,7 @@ function findJSFiles(dir, rootDir = dir) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Extract function signatures from a file
|
|
65
65
|
* @param {string} filePath
|
|
66
|
+
* @param {string} rootDir - Root directory for relative path calculation
|
|
66
67
|
* @returns {FunctionSignature[]}
|
|
67
68
|
*/
|
|
68
69
|
function extractSignatures(filePath, rootDir) {
|