@optave/codegraph 2.4.0 → 2.5.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/README.md +66 -10
- package/package.json +15 -5
- package/src/branch-compare.js +568 -0
- package/src/builder.js +183 -22
- package/src/cli.js +253 -8
- package/src/cochange.js +8 -8
- package/src/communities.js +303 -0
- package/src/complexity.js +2056 -0
- package/src/config.js +20 -1
- package/src/db.js +111 -1
- package/src/embedder.js +49 -12
- package/src/export.js +25 -1
- package/src/flow.js +361 -0
- package/src/index.js +32 -2
- package/src/manifesto.js +442 -0
- package/src/mcp.js +244 -5
- package/src/paginate.js +70 -0
- package/src/parser.js +21 -5
- package/src/queries.js +396 -7
- package/src/registry.js +6 -3
- package/src/structure.js +88 -24
- package/src/update-check.js +1 -0
- package/src/watcher.js +2 -2
package/src/mcp.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { findCycles } from './cycles.js';
|
|
10
10
|
import { findDbPath } from './db.js';
|
|
11
|
+
import { MCP_DEFAULTS, MCP_MAX_LIMIT } from './paginate.js';
|
|
11
12
|
import { ALL_SYMBOL_KINDS, diffImpactMermaid, VALID_ROLES } from './queries.js';
|
|
12
13
|
|
|
13
14
|
const REPO_PROP = {
|
|
@@ -17,6 +18,11 @@ const REPO_PROP = {
|
|
|
17
18
|
},
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
const PAGINATION_PROPS = {
|
|
22
|
+
limit: { type: 'number', description: 'Max results to return (pagination)' },
|
|
23
|
+
offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
|
|
24
|
+
};
|
|
25
|
+
|
|
20
26
|
const BASE_TOOLS = [
|
|
21
27
|
{
|
|
22
28
|
name: 'query_function',
|
|
@@ -31,6 +37,7 @@ const BASE_TOOLS = [
|
|
|
31
37
|
default: 2,
|
|
32
38
|
},
|
|
33
39
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
40
|
+
...PAGINATION_PROPS,
|
|
34
41
|
},
|
|
35
42
|
required: ['name'],
|
|
36
43
|
},
|
|
@@ -123,6 +130,33 @@ const BASE_TOOLS = [
|
|
|
123
130
|
required: ['name'],
|
|
124
131
|
},
|
|
125
132
|
},
|
|
133
|
+
{
|
|
134
|
+
name: 'symbol_path',
|
|
135
|
+
description: 'Find the shortest path between two symbols in the call graph (A calls...calls B)',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
from: { type: 'string', description: 'Source symbol name (partial match)' },
|
|
140
|
+
to: { type: 'string', description: 'Target symbol name (partial match)' },
|
|
141
|
+
max_depth: { type: 'number', description: 'Maximum BFS depth', default: 10 },
|
|
142
|
+
edge_kinds: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
items: { type: 'string' },
|
|
145
|
+
description: 'Edge kinds to follow (default: ["calls"])',
|
|
146
|
+
},
|
|
147
|
+
reverse: { type: 'boolean', description: 'Follow edges backward', default: false },
|
|
148
|
+
from_file: { type: 'string', description: 'Disambiguate source by file (partial match)' },
|
|
149
|
+
to_file: { type: 'string', description: 'Disambiguate target by file (partial match)' },
|
|
150
|
+
kind: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
enum: ALL_SYMBOL_KINDS,
|
|
153
|
+
description: 'Filter both symbols by kind',
|
|
154
|
+
},
|
|
155
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
156
|
+
},
|
|
157
|
+
required: ['from', 'to'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
126
160
|
{
|
|
127
161
|
name: 'context',
|
|
128
162
|
description:
|
|
@@ -187,6 +221,7 @@ const BASE_TOOLS = [
|
|
|
187
221
|
default: false,
|
|
188
222
|
},
|
|
189
223
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
224
|
+
...PAGINATION_PROPS,
|
|
190
225
|
},
|
|
191
226
|
required: ['target'],
|
|
192
227
|
},
|
|
@@ -239,6 +274,7 @@ const BASE_TOOLS = [
|
|
|
239
274
|
description: 'File-level graph (true) or function-level (false)',
|
|
240
275
|
default: true,
|
|
241
276
|
},
|
|
277
|
+
...PAGINATION_PROPS,
|
|
242
278
|
},
|
|
243
279
|
required: ['format'],
|
|
244
280
|
},
|
|
@@ -253,13 +289,14 @@ const BASE_TOOLS = [
|
|
|
253
289
|
file: { type: 'string', description: 'Filter by file path (partial match)' },
|
|
254
290
|
pattern: { type: 'string', description: 'Filter by function name (partial match)' },
|
|
255
291
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
292
|
+
...PAGINATION_PROPS,
|
|
256
293
|
},
|
|
257
294
|
},
|
|
258
295
|
},
|
|
259
296
|
{
|
|
260
297
|
name: 'structure',
|
|
261
298
|
description:
|
|
262
|
-
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics',
|
|
299
|
+
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics. Per-file details are capped at 25 files by default; use full=true to show all.',
|
|
263
300
|
inputSchema: {
|
|
264
301
|
type: 'object',
|
|
265
302
|
properties: {
|
|
@@ -270,6 +307,11 @@ const BASE_TOOLS = [
|
|
|
270
307
|
enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
|
|
271
308
|
description: 'Sort directories by metric',
|
|
272
309
|
},
|
|
310
|
+
full: {
|
|
311
|
+
type: 'boolean',
|
|
312
|
+
description: 'Return all files without limit',
|
|
313
|
+
default: false,
|
|
314
|
+
},
|
|
273
315
|
},
|
|
274
316
|
},
|
|
275
317
|
},
|
|
@@ -287,6 +329,7 @@ const BASE_TOOLS = [
|
|
|
287
329
|
},
|
|
288
330
|
file: { type: 'string', description: 'Scope to a specific file (partial match)' },
|
|
289
331
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
332
|
+
...PAGINATION_PROPS,
|
|
290
333
|
},
|
|
291
334
|
},
|
|
292
335
|
},
|
|
@@ -333,6 +376,121 @@ const BASE_TOOLS = [
|
|
|
333
376
|
},
|
|
334
377
|
},
|
|
335
378
|
},
|
|
379
|
+
{
|
|
380
|
+
name: 'execution_flow',
|
|
381
|
+
description:
|
|
382
|
+
'Trace execution flow forward from an entry point (route, command, event) through callees to leaf functions. Answers "what happens when X is called?"',
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {
|
|
386
|
+
name: {
|
|
387
|
+
type: 'string',
|
|
388
|
+
description:
|
|
389
|
+
'Entry point or function name (e.g. "POST /login", "build"). Supports prefix-stripped matching.',
|
|
390
|
+
},
|
|
391
|
+
depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
|
|
392
|
+
file: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
395
|
+
},
|
|
396
|
+
kind: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
enum: ALL_SYMBOL_KINDS,
|
|
399
|
+
description: 'Filter to a specific symbol kind',
|
|
400
|
+
},
|
|
401
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
402
|
+
},
|
|
403
|
+
required: ['name'],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'list_entry_points',
|
|
408
|
+
description:
|
|
409
|
+
'List all framework entry points (routes, commands, events) in the codebase, grouped by type',
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
properties: {
|
|
413
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
414
|
+
...PAGINATION_PROPS,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: 'complexity',
|
|
420
|
+
description:
|
|
421
|
+
'Show per-function complexity metrics (cognitive, cyclomatic, nesting, Halstead, Maintainability Index). Sorted by most complex first.',
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: {
|
|
425
|
+
name: { type: 'string', description: 'Function name filter (partial match)' },
|
|
426
|
+
file: { type: 'string', description: 'Scope to file (partial match)' },
|
|
427
|
+
limit: { type: 'number', description: 'Max results', default: 20 },
|
|
428
|
+
sort: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
enum: ['cognitive', 'cyclomatic', 'nesting', 'mi', 'volume', 'effort', 'bugs', 'loc'],
|
|
431
|
+
description: 'Sort metric',
|
|
432
|
+
default: 'cognitive',
|
|
433
|
+
},
|
|
434
|
+
above_threshold: {
|
|
435
|
+
type: 'boolean',
|
|
436
|
+
description: 'Only functions exceeding warn thresholds',
|
|
437
|
+
default: false,
|
|
438
|
+
},
|
|
439
|
+
health: {
|
|
440
|
+
type: 'boolean',
|
|
441
|
+
description: 'Include Halstead and Maintainability Index metrics',
|
|
442
|
+
default: false,
|
|
443
|
+
},
|
|
444
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
445
|
+
kind: {
|
|
446
|
+
type: 'string',
|
|
447
|
+
description: 'Filter by symbol kind (function, method, class, etc.)',
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: 'manifesto',
|
|
454
|
+
description:
|
|
455
|
+
'Evaluate manifesto rules and return pass/fail verdicts for code health. Checks function complexity, file metrics, and cycle rules against configured thresholds.',
|
|
456
|
+
inputSchema: {
|
|
457
|
+
type: 'object',
|
|
458
|
+
properties: {
|
|
459
|
+
file: { type: 'string', description: 'Scope to file (partial match)' },
|
|
460
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
461
|
+
kind: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
description: 'Filter by symbol kind (function, method, class, etc.)',
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'communities',
|
|
470
|
+
description:
|
|
471
|
+
'Detect natural module boundaries using Louvain community detection. Compares discovered communities against directory structure and surfaces architectural drift.',
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
functions: {
|
|
476
|
+
type: 'boolean',
|
|
477
|
+
description: 'Function-level instead of file-level',
|
|
478
|
+
default: false,
|
|
479
|
+
},
|
|
480
|
+
resolution: {
|
|
481
|
+
type: 'number',
|
|
482
|
+
description: 'Louvain resolution parameter (higher = more communities)',
|
|
483
|
+
default: 1.0,
|
|
484
|
+
},
|
|
485
|
+
drift: {
|
|
486
|
+
type: 'boolean',
|
|
487
|
+
description: 'Show only drift analysis (omit community member lists)',
|
|
488
|
+
default: false,
|
|
489
|
+
},
|
|
490
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
336
494
|
];
|
|
337
495
|
|
|
338
496
|
const LIST_REPOS_TOOL = {
|
|
@@ -405,6 +563,7 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
405
563
|
fileDepsData,
|
|
406
564
|
fnDepsData,
|
|
407
565
|
fnImpactData,
|
|
566
|
+
pathData,
|
|
408
567
|
contextData,
|
|
409
568
|
explainData,
|
|
410
569
|
whereData,
|
|
@@ -457,7 +616,11 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
457
616
|
let result;
|
|
458
617
|
switch (name) {
|
|
459
618
|
case 'query_function':
|
|
460
|
-
result = queryNameData(args.name, dbPath, {
|
|
619
|
+
result = queryNameData(args.name, dbPath, {
|
|
620
|
+
noTests: args.no_tests,
|
|
621
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.query_function, MCP_MAX_LIMIT),
|
|
622
|
+
offset: args.offset ?? 0,
|
|
623
|
+
});
|
|
461
624
|
break;
|
|
462
625
|
case 'file_deps':
|
|
463
626
|
result = fileDepsData(args.file, dbPath, { noTests: args.no_tests });
|
|
@@ -491,6 +654,17 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
491
654
|
noTests: args.no_tests,
|
|
492
655
|
});
|
|
493
656
|
break;
|
|
657
|
+
case 'symbol_path':
|
|
658
|
+
result = pathData(args.from, args.to, dbPath, {
|
|
659
|
+
maxDepth: args.max_depth,
|
|
660
|
+
edgeKinds: args.edge_kinds,
|
|
661
|
+
reverse: args.reverse,
|
|
662
|
+
fromFile: args.from_file,
|
|
663
|
+
toFile: args.to_file,
|
|
664
|
+
kind: args.kind,
|
|
665
|
+
noTests: args.no_tests,
|
|
666
|
+
});
|
|
667
|
+
break;
|
|
494
668
|
case 'context':
|
|
495
669
|
result = contextData(args.name, dbPath, {
|
|
496
670
|
depth: args.depth,
|
|
@@ -508,6 +682,8 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
508
682
|
result = whereData(args.target, dbPath, {
|
|
509
683
|
file: args.file_mode,
|
|
510
684
|
noTests: args.no_tests,
|
|
685
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.where, MCP_MAX_LIMIT),
|
|
686
|
+
offset: args.offset ?? 0,
|
|
511
687
|
});
|
|
512
688
|
break;
|
|
513
689
|
case 'diff_impact':
|
|
@@ -547,15 +723,21 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
547
723
|
const { exportDOT, exportMermaid, exportJSON } = await import('./export.js');
|
|
548
724
|
const db = new Database(findDbPath(dbPath), { readonly: true });
|
|
549
725
|
const fileLevel = args.file_level !== false;
|
|
726
|
+
const exportLimit = args.limit
|
|
727
|
+
? Math.min(args.limit, MCP_MAX_LIMIT)
|
|
728
|
+
: MCP_DEFAULTS.export_graph;
|
|
550
729
|
switch (args.format) {
|
|
551
730
|
case 'dot':
|
|
552
|
-
result = exportDOT(db, { fileLevel });
|
|
731
|
+
result = exportDOT(db, { fileLevel, limit: exportLimit });
|
|
553
732
|
break;
|
|
554
733
|
case 'mermaid':
|
|
555
|
-
result = exportMermaid(db, { fileLevel });
|
|
734
|
+
result = exportMermaid(db, { fileLevel, limit: exportLimit });
|
|
556
735
|
break;
|
|
557
736
|
case 'json':
|
|
558
|
-
result = exportJSON(db
|
|
737
|
+
result = exportJSON(db, {
|
|
738
|
+
limit: exportLimit,
|
|
739
|
+
offset: args.offset ?? 0,
|
|
740
|
+
});
|
|
559
741
|
break;
|
|
560
742
|
default:
|
|
561
743
|
db.close();
|
|
@@ -577,6 +759,8 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
577
759
|
file: args.file,
|
|
578
760
|
pattern: args.pattern,
|
|
579
761
|
noTests: args.no_tests,
|
|
762
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.list_functions, MCP_MAX_LIMIT),
|
|
763
|
+
offset: args.offset ?? 0,
|
|
580
764
|
});
|
|
581
765
|
break;
|
|
582
766
|
case 'node_roles':
|
|
@@ -584,6 +768,8 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
584
768
|
role: args.role,
|
|
585
769
|
file: args.file,
|
|
586
770
|
noTests: args.no_tests,
|
|
771
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.node_roles, MCP_MAX_LIMIT),
|
|
772
|
+
offset: args.offset ?? 0,
|
|
587
773
|
});
|
|
588
774
|
break;
|
|
589
775
|
case 'structure': {
|
|
@@ -592,6 +778,7 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
592
778
|
directory: args.directory,
|
|
593
779
|
depth: args.depth,
|
|
594
780
|
sort: args.sort,
|
|
781
|
+
full: args.full,
|
|
595
782
|
});
|
|
596
783
|
break;
|
|
597
784
|
}
|
|
@@ -620,6 +807,58 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
620
807
|
});
|
|
621
808
|
break;
|
|
622
809
|
}
|
|
810
|
+
case 'execution_flow': {
|
|
811
|
+
const { flowData } = await import('./flow.js');
|
|
812
|
+
result = flowData(args.name, dbPath, {
|
|
813
|
+
depth: args.depth,
|
|
814
|
+
file: args.file,
|
|
815
|
+
kind: args.kind,
|
|
816
|
+
noTests: args.no_tests,
|
|
817
|
+
});
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case 'list_entry_points': {
|
|
821
|
+
const { listEntryPointsData } = await import('./flow.js');
|
|
822
|
+
result = listEntryPointsData(dbPath, {
|
|
823
|
+
noTests: args.no_tests,
|
|
824
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.list_entry_points, MCP_MAX_LIMIT),
|
|
825
|
+
offset: args.offset ?? 0,
|
|
826
|
+
});
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case 'complexity': {
|
|
830
|
+
const { complexityData } = await import('./complexity.js');
|
|
831
|
+
result = complexityData(dbPath, {
|
|
832
|
+
target: args.name,
|
|
833
|
+
file: args.file,
|
|
834
|
+
limit: args.limit,
|
|
835
|
+
sort: args.sort,
|
|
836
|
+
aboveThreshold: args.above_threshold,
|
|
837
|
+
health: args.health,
|
|
838
|
+
noTests: args.no_tests,
|
|
839
|
+
kind: args.kind,
|
|
840
|
+
});
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case 'manifesto': {
|
|
844
|
+
const { manifestoData } = await import('./manifesto.js');
|
|
845
|
+
result = manifestoData(dbPath, {
|
|
846
|
+
file: args.file,
|
|
847
|
+
noTests: args.no_tests,
|
|
848
|
+
kind: args.kind,
|
|
849
|
+
});
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
case 'communities': {
|
|
853
|
+
const { communitiesData } = await import('./communities.js');
|
|
854
|
+
result = communitiesData(dbPath, {
|
|
855
|
+
functions: args.functions,
|
|
856
|
+
resolution: args.resolution,
|
|
857
|
+
drift: args.drift,
|
|
858
|
+
noTests: args.no_tests,
|
|
859
|
+
});
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
623
862
|
case 'list_repos': {
|
|
624
863
|
const { listRepos, pruneRegistry } = await import('./registry.js');
|
|
625
864
|
pruneRegistry();
|
package/src/paginate.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination utilities for bounded, context-friendly query results.
|
|
3
|
+
*
|
|
4
|
+
* Offset/limit pagination — the DB is a read-only snapshot so data doesn't
|
|
5
|
+
* change between pages; offset/limit is simpler and maps directly to SQL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Default limits applied by MCP tool handlers (not by the programmatic API). */
|
|
9
|
+
export const MCP_DEFAULTS = {
|
|
10
|
+
list_functions: 100,
|
|
11
|
+
query_function: 50,
|
|
12
|
+
where: 50,
|
|
13
|
+
node_roles: 100,
|
|
14
|
+
list_entry_points: 100,
|
|
15
|
+
export_graph: 500,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Hard cap to prevent abuse via MCP. */
|
|
19
|
+
export const MCP_MAX_LIMIT = 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Paginate an array.
|
|
23
|
+
*
|
|
24
|
+
* When `limit` is undefined the input is returned unchanged (no-op).
|
|
25
|
+
*
|
|
26
|
+
* @param {any[]} items
|
|
27
|
+
* @param {{ limit?: number, offset?: number }} opts
|
|
28
|
+
* @returns {{ items: any[], pagination?: { total: number, offset: number, limit: number, hasMore: boolean, returned: number } }}
|
|
29
|
+
*/
|
|
30
|
+
export function paginate(items, { limit, offset } = {}) {
|
|
31
|
+
if (limit === undefined) {
|
|
32
|
+
return { items };
|
|
33
|
+
}
|
|
34
|
+
const total = items.length;
|
|
35
|
+
const off = Math.max(0, Math.min(offset || 0, total));
|
|
36
|
+
const lim = Math.max(0, limit);
|
|
37
|
+
const page = items.slice(off, off + lim);
|
|
38
|
+
return {
|
|
39
|
+
items: page,
|
|
40
|
+
pagination: {
|
|
41
|
+
total,
|
|
42
|
+
offset: off,
|
|
43
|
+
limit: lim,
|
|
44
|
+
hasMore: off + lim < total,
|
|
45
|
+
returned: page.length,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply pagination to a named array field on a result object.
|
|
52
|
+
*
|
|
53
|
+
* When `limit` is undefined the result is returned unchanged (backward compat).
|
|
54
|
+
* When active, `_pagination` metadata is added to the result.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} result - The result object (e.g. `{ count: 42, functions: [...] }`)
|
|
57
|
+
* @param {string} field - The array field name to paginate (e.g. `'functions'`)
|
|
58
|
+
* @param {{ limit?: number, offset?: number }} opts
|
|
59
|
+
* @returns {object} - Result with paginated field + `_pagination` (if active)
|
|
60
|
+
*/
|
|
61
|
+
export function paginateResult(result, field, { limit, offset } = {}) {
|
|
62
|
+
if (limit === undefined) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
const arr = result[field];
|
|
66
|
+
if (!Array.isArray(arr)) return result;
|
|
67
|
+
|
|
68
|
+
const { items, pagination } = paginate(arr, { limit, offset });
|
|
69
|
+
return { ...result, [field]: items, _pagination: pagination };
|
|
70
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -125,12 +125,23 @@ function resolveEngine(opts = {}) {
|
|
|
125
125
|
*/
|
|
126
126
|
function normalizeNativeSymbols(result) {
|
|
127
127
|
return {
|
|
128
|
+
_lineCount: result.lineCount ?? result.line_count ?? null,
|
|
128
129
|
definitions: (result.definitions || []).map((d) => ({
|
|
129
130
|
name: d.name,
|
|
130
131
|
kind: d.kind,
|
|
131
132
|
line: d.line,
|
|
132
133
|
endLine: d.endLine ?? d.end_line ?? null,
|
|
133
134
|
decorators: d.decorators,
|
|
135
|
+
complexity: d.complexity
|
|
136
|
+
? {
|
|
137
|
+
cognitive: d.complexity.cognitive,
|
|
138
|
+
cyclomatic: d.complexity.cyclomatic,
|
|
139
|
+
maxNesting: d.complexity.maxNesting,
|
|
140
|
+
halstead: d.complexity.halstead ?? null,
|
|
141
|
+
loc: d.complexity.loc ?? null,
|
|
142
|
+
maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
|
|
143
|
+
}
|
|
144
|
+
: null,
|
|
134
145
|
})),
|
|
135
146
|
calls: (result.calls || []).map((c) => ({
|
|
136
147
|
name: c.name,
|
|
@@ -279,7 +290,8 @@ function wasmExtractSymbols(parsers, filePath, code) {
|
|
|
279
290
|
const entry = _extToLang.get(ext);
|
|
280
291
|
if (!entry) return null;
|
|
281
292
|
const query = _queryCache.get(entry.id) || null;
|
|
282
|
-
|
|
293
|
+
const symbols = entry.extractor(tree, filePath, query);
|
|
294
|
+
return symbols ? { symbols, tree, langId: entry.id } : null;
|
|
283
295
|
}
|
|
284
296
|
|
|
285
297
|
/**
|
|
@@ -300,7 +312,8 @@ export async function parseFileAuto(filePath, source, opts = {}) {
|
|
|
300
312
|
|
|
301
313
|
// WASM path
|
|
302
314
|
const parsers = await createParsers();
|
|
303
|
-
|
|
315
|
+
const extracted = wasmExtractSymbols(parsers, filePath, source);
|
|
316
|
+
return extracted ? extracted.symbols : null;
|
|
304
317
|
}
|
|
305
318
|
|
|
306
319
|
/**
|
|
@@ -335,10 +348,13 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
|
|
|
335
348
|
warn(`Skipping ${path.relative(rootDir, filePath)}: ${err.message}`);
|
|
336
349
|
continue;
|
|
337
350
|
}
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
351
|
+
const extracted = wasmExtractSymbols(parsers, filePath, code);
|
|
352
|
+
if (extracted) {
|
|
340
353
|
const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
341
|
-
|
|
354
|
+
extracted.symbols._tree = extracted.tree;
|
|
355
|
+
extracted.symbols._langId = extracted.langId;
|
|
356
|
+
extracted.symbols._lineCount = code.split('\n').length;
|
|
357
|
+
result.set(relPath, extracted.symbols);
|
|
342
358
|
}
|
|
343
359
|
}
|
|
344
360
|
return result;
|