@mishasinitcyn/betterrank 0.1.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/src/index.js ADDED
@@ -0,0 +1,615 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { join, dirname, relative, sep } from 'path';
3
+ import { CodeIndexCache } from './cache.js';
4
+ import { rankedSymbols } from './graph.js';
5
+
6
+ /**
7
+ * Apply offset/limit pagination to an array.
8
+ * Returns { items, total } where total is the unpaginated count.
9
+ */
10
+ function paginate(arr, { offset = 0, limit } = {}) {
11
+ const total = arr.length;
12
+ const start = Math.max(0, offset);
13
+ const items = limit !== undefined ? arr.slice(start, start + limit) : arr.slice(start);
14
+ return { items, total };
15
+ }
16
+
17
+ class CodeIndex {
18
+ constructor(projectRoot, opts = {}) {
19
+ this.projectRoot = projectRoot;
20
+ this.cache = new CodeIndexCache(projectRoot, opts);
21
+ this._rankedCache = null;
22
+ this._fileScoresCache = null;
23
+ }
24
+
25
+ async _ensureReady() {
26
+ const result = await this.cache.ensure();
27
+ if (result.changed > 0 || result.deleted > 0) {
28
+ this._rankedCache = null;
29
+ this._fileScoresCache = null;
30
+ }
31
+ return result;
32
+ }
33
+
34
+ /**
35
+ * Lazy-cached PageRank scores for all symbols.
36
+ * Unfocused (no bias) results are cached for the session.
37
+ */
38
+ _getRanked(focusFiles = []) {
39
+ if (focusFiles.length === 0 && this._rankedCache) {
40
+ return this._rankedCache;
41
+ }
42
+ const graph = this.cache.getGraph();
43
+ if (!graph || graph.order === 0) return [];
44
+ const ranked = rankedSymbols(graph, focusFiles);
45
+ if (focusFiles.length === 0) {
46
+ this._rankedCache = ranked;
47
+ }
48
+ return ranked;
49
+ }
50
+
51
+ /**
52
+ * Lazy-cached file-level PageRank scores (sum of symbol scores per file).
53
+ */
54
+ _getFileScores() {
55
+ if (this._fileScoresCache) return this._fileScoresCache;
56
+ const ranked = this._getRanked();
57
+ const graph = this.cache.getGraph();
58
+ const scores = new Map();
59
+ for (const [symbolKey, score] of ranked) {
60
+ try {
61
+ const attrs = graph.getNodeAttributes(symbolKey);
62
+ if (attrs.type !== 'symbol') continue;
63
+ scores.set(attrs.file, (scores.get(attrs.file) || 0) + score);
64
+ } catch { continue; }
65
+ }
66
+ this._fileScoresCache = scores;
67
+ return scores;
68
+ }
69
+
70
+ /**
71
+ * Aider-style repo map: a compact summary of the most structurally
72
+ * important definitions and their signatures, ranked by PageRank.
73
+ *
74
+ * @param {object} opts
75
+ * @param {string[]} [opts.focusFiles] - Bias ranking toward these files
76
+ * @param {number} [opts.offset] - Skip first N symbols
77
+ * @param {number} [opts.limit] - Max symbols to return (default: 50)
78
+ * @param {boolean} [opts.count] - If true, return only { total }
79
+ * @returns {{content, shownFiles, shownSymbols, totalFiles, totalSymbols}|{total: number}}
80
+ */
81
+ async map({ focusFiles = [], offset, limit, count = false } = {}) {
82
+ await this._ensureReady();
83
+ const graph = this.cache.getGraph();
84
+ if (!graph || graph.order === 0) {
85
+ return count
86
+ ? { total: 0 }
87
+ : { content: '(empty index)', shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0 };
88
+ }
89
+
90
+ // Count totals from the graph
91
+ let totalFiles = 0;
92
+ let totalSymbols = 0;
93
+ graph.forEachNode((_node, attrs) => {
94
+ if (attrs.type === 'file') totalFiles++;
95
+ else if (attrs.type === 'symbol') totalSymbols++;
96
+ });
97
+
98
+ const ranked = this._getRanked(focusFiles);
99
+
100
+ // Collect all symbol entries ranked by PageRank
101
+ const allEntries = [];
102
+ for (const [symbolKey, _score] of ranked) {
103
+ let attrs;
104
+ try {
105
+ attrs = graph.getNodeAttributes(symbolKey);
106
+ } catch {
107
+ continue;
108
+ }
109
+ if (attrs.type !== 'symbol') continue;
110
+
111
+ const line = ` ${String(attrs.lineStart).padStart(4)}│ ${attrs.signature}`;
112
+ allEntries.push({ file: attrs.file, line });
113
+ }
114
+
115
+ if (count) return { total: allEntries.length };
116
+
117
+ const { items } = paginate(allEntries, { offset, limit });
118
+
119
+ const fileGroups = new Map();
120
+ for (const entry of items) {
121
+ if (!fileGroups.has(entry.file)) fileGroups.set(entry.file, []);
122
+ fileGroups.get(entry.file).push(entry.line);
123
+ }
124
+
125
+ const lines = [];
126
+ for (const [file, entries] of fileGroups) {
127
+ lines.push(`${file}:`);
128
+ lines.push(...entries);
129
+ lines.push('');
130
+ }
131
+
132
+ return {
133
+ content: lines.join('\n').trimEnd(),
134
+ shownFiles: fileGroups.size,
135
+ shownSymbols: items.length,
136
+ totalFiles,
137
+ totalSymbols,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * File tree with symbol counts per file.
143
+ *
144
+ * @param {object} [opts]
145
+ * @param {number} [opts.depth] - Max directory depth (default unlimited)
146
+ * @returns {string} Formatted tree
147
+ */
148
+ async structure({ depth } = {}) {
149
+ await this._ensureReady();
150
+ const graph = this.cache.getGraph();
151
+ if (!graph || graph.order === 0) return '(empty index)';
152
+
153
+ const fileNodes = [];
154
+ graph.forEachNode((node, attrs) => {
155
+ if (attrs.type === 'file') {
156
+ fileNodes.push({ path: node, symbolCount: attrs.symbolCount || 0 });
157
+ }
158
+ });
159
+
160
+ fileNodes.sort((a, b) => a.path.localeCompare(b.path));
161
+
162
+ const tree = {};
163
+ for (const { path: filePath, symbolCount } of fileNodes) {
164
+ const parts = filePath.split(/[/\\]/);
165
+ if (depth !== undefined && parts.length > depth + 1) continue;
166
+
167
+ let current = tree;
168
+ for (let i = 0; i < parts.length - 1; i++) {
169
+ if (!current[parts[i]]) current[parts[i]] = {};
170
+ current = current[parts[i]];
171
+ }
172
+ current[parts[parts.length - 1]] = symbolCount;
173
+ }
174
+
175
+ return formatTree(tree, '');
176
+ }
177
+
178
+ /**
179
+ * Fuzzy-search symbols by substring matching against name and signature
180
+ * (which includes parameter names and types). Results ranked by PageRank.
181
+ *
182
+ * @param {object} opts
183
+ * @param {string} opts.query - Substring to match (case-insensitive)
184
+ * @param {string} [opts.kind] - Filter to this kind (function, class, type, variable)
185
+ * @param {number} [opts.offset] - Skip first N results
186
+ * @param {number} [opts.limit] - Max results to return
187
+ * @param {boolean} [opts.count] - If true, return only { total }
188
+ * @returns {Array|{total: number}}
189
+ */
190
+ async search({ query, kind, offset, limit, count = false }) {
191
+ await this._ensureReady();
192
+ const graph = this.cache.getGraph();
193
+ if (!graph || graph.order === 0) return count ? { total: 0 } : [];
194
+
195
+ const ranked = this._getRanked();
196
+ const scoreMap = new Map(ranked);
197
+ const q = query.toLowerCase();
198
+
199
+ const results = [];
200
+ graph.forEachNode((node, attrs) => {
201
+ if (attrs.type !== 'symbol') return;
202
+ if (kind && attrs.kind !== kind) return;
203
+
204
+ const nameMatch = attrs.name.toLowerCase().includes(q);
205
+ const sigMatch = attrs.signature && attrs.signature.toLowerCase().includes(q);
206
+ if (!nameMatch && !sigMatch) return;
207
+
208
+ results.push({
209
+ name: attrs.name,
210
+ kind: attrs.kind,
211
+ file: attrs.file,
212
+ lineStart: attrs.lineStart,
213
+ lineEnd: attrs.lineEnd,
214
+ signature: attrs.signature,
215
+ _score: scoreMap.get(node) || 0,
216
+ });
217
+ });
218
+
219
+ results.sort((a, b) => b._score - a._score);
220
+ for (const r of results) delete r._score;
221
+
222
+ if (count) return { total: results.length };
223
+ return paginate(results, { offset, limit }).items;
224
+ }
225
+
226
+ /**
227
+ * List symbol definitions, optionally filtered.
228
+ * Results are ranked by PageRank (most structurally important first).
229
+ * Supports offset/limit pagination and count-only mode.
230
+ *
231
+ * @param {object} [opts]
232
+ * @param {string} [opts.file] - Filter to this file
233
+ * @param {string} [opts.kind] - Filter to this kind (function, class, type, variable)
234
+ * @param {number} [opts.offset] - Skip first N results
235
+ * @param {number} [opts.limit] - Max results to return
236
+ * @param {boolean} [opts.count] - If true, return only { total }
237
+ * @returns {Array|{total: number}}
238
+ */
239
+ async symbols({ file, kind, offset, limit, count = false } = {}) {
240
+ await this._ensureReady();
241
+ const graph = this.cache.getGraph();
242
+ if (!graph || graph.order === 0) return count ? { total: 0 } : [];
243
+
244
+ const ranked = this._getRanked();
245
+ const scoreMap = new Map(ranked);
246
+
247
+ const results = [];
248
+ graph.forEachNode((node, attrs) => {
249
+ if (attrs.type !== 'symbol') return;
250
+ if (file && attrs.file !== file) return;
251
+ if (kind && attrs.kind !== kind) return;
252
+ results.push({
253
+ name: attrs.name,
254
+ kind: attrs.kind,
255
+ file: attrs.file,
256
+ lineStart: attrs.lineStart,
257
+ lineEnd: attrs.lineEnd,
258
+ signature: attrs.signature,
259
+ _score: scoreMap.get(node) || 0,
260
+ });
261
+ });
262
+
263
+ results.sort((a, b) => b._score - a._score);
264
+ for (const r of results) delete r._score;
265
+
266
+ if (count) return { total: results.length };
267
+ return paginate(results, { offset, limit }).items;
268
+ }
269
+
270
+ /**
271
+ * All call sites of a symbol across the codebase.
272
+ * Results are ranked by file-level PageRank (most important callers first).
273
+ * Supports offset/limit pagination and count-only mode.
274
+ *
275
+ * @param {object} opts
276
+ * @param {string} opts.symbol - Symbol name
277
+ * @param {string} [opts.file] - Disambiguate by file
278
+ * @param {number} [opts.offset] - Skip first N results
279
+ * @param {number} [opts.limit] - Max results to return
280
+ * @param {boolean} [opts.count] - If true, return only { total }
281
+ * @returns {Array<{file}>|{total: number}}
282
+ */
283
+ async callers({ symbol, file, offset, limit, count = false }) {
284
+ await this._ensureReady();
285
+ const graph = this.cache.getGraph();
286
+ if (!graph) return count ? { total: 0 } : [];
287
+
288
+ const fileScores = this._getFileScores();
289
+
290
+ const targetKeys = [];
291
+ graph.forEachNode((node, attrs) => {
292
+ if (attrs.type !== 'symbol') return;
293
+ if (attrs.name !== symbol) return;
294
+ if (file && attrs.file !== file) return;
295
+ targetKeys.push(node);
296
+ });
297
+
298
+ const callerSet = new Set();
299
+ const results = [];
300
+
301
+ for (const targetKey of targetKeys) {
302
+ graph.forEachInEdge(targetKey, (edge, attrs, source) => {
303
+ if (attrs.type !== 'REFERENCES') return;
304
+ const sourceAttrs = graph.getNodeAttributes(source);
305
+ const callerFile = sourceAttrs.file || source;
306
+ if (!callerSet.has(callerFile)) {
307
+ callerSet.add(callerFile);
308
+ results.push({ file: callerFile, _score: fileScores.get(callerFile) || 0 });
309
+ }
310
+ });
311
+ }
312
+
313
+ results.sort((a, b) => b._score - a._score);
314
+ for (const r of results) delete r._score;
315
+
316
+ if (count) return { total: results.length };
317
+ return paginate(results, { offset, limit }).items;
318
+ }
319
+
320
+ /**
321
+ * What this file imports / depends on.
322
+ * Results are ranked by file-level PageRank (most important dependencies first).
323
+ * Supports offset/limit pagination and count-only mode.
324
+ *
325
+ * @param {object} opts
326
+ * @param {string} opts.file - File path (relative to project root)
327
+ * @param {number} [opts.offset] - Skip first N results
328
+ * @param {number} [opts.limit] - Max results to return
329
+ * @param {boolean} [opts.count] - If true, return only { total }
330
+ * @returns {string[]|{total: number}}
331
+ */
332
+ async dependencies({ file, offset, limit, count = false }) {
333
+ await this._ensureReady();
334
+ const graph = this.cache.getGraph();
335
+ if (!graph || !graph.hasNode(file)) return count ? { total: 0 } : [];
336
+
337
+ const fileScores = this._getFileScores();
338
+
339
+ const deps = new Set();
340
+ graph.forEachOutEdge(file, (edge, attrs, _source, target) => {
341
+ if (attrs.type === 'IMPORTS') {
342
+ const targetAttrs = graph.getNodeAttributes(target);
343
+ if (targetAttrs.type === 'file') {
344
+ deps.add(target);
345
+ }
346
+ }
347
+ });
348
+
349
+ const sorted = [...deps].sort((a, b) => (fileScores.get(b) || 0) - (fileScores.get(a) || 0));
350
+ if (count) return { total: sorted.length };
351
+ return paginate(sorted, { offset, limit }).items;
352
+ }
353
+
354
+ /**
355
+ * What depends on this file.
356
+ * Results are ranked by file-level PageRank (most important dependents first).
357
+ * Supports offset/limit pagination and count-only mode.
358
+ *
359
+ * @param {object} opts
360
+ * @param {string} opts.file - File path (relative to project root)
361
+ * @param {number} [opts.offset] - Skip first N results
362
+ * @param {number} [opts.limit] - Max results to return
363
+ * @param {boolean} [opts.count] - If true, return only { total }
364
+ * @returns {string[]|{total: number}}
365
+ */
366
+ async dependents({ file, offset, limit, count = false }) {
367
+ await this._ensureReady();
368
+ const graph = this.cache.getGraph();
369
+ if (!graph || !graph.hasNode(file)) return count ? { total: 0 } : [];
370
+
371
+ const fileScores = this._getFileScores();
372
+
373
+ const deps = new Set();
374
+ graph.forEachInEdge(file, (edge, attrs, source) => {
375
+ if (attrs.type === 'IMPORTS') {
376
+ const sourceAttrs = graph.getNodeAttributes(source);
377
+ if (sourceAttrs.type === 'file') {
378
+ deps.add(source);
379
+ }
380
+ }
381
+ });
382
+
383
+ const sorted = [...deps].sort((a, b) => (fileScores.get(b) || 0) - (fileScores.get(a) || 0));
384
+ if (count) return { total: sorted.length };
385
+ return paginate(sorted, { offset, limit }).items;
386
+ }
387
+
388
+ /**
389
+ * File-level dependency neighborhood with PageRank-ranked symbol signatures.
390
+ *
391
+ * BFS walks outgoing IMPORTS edges (dependencies). Optionally includes
392
+ * direct dependents (files that import the starting file).
393
+ *
394
+ * Files are aggressively ranked by PageRank. Only the starting file's
395
+ * direct neighbors are guaranteed; further-hop files must earn their
396
+ * place via PageRank score. Default cap is 15 files.
397
+ *
398
+ * Supports count-only mode (returns sizes without content) and
399
+ * offset/limit pagination on files (symbols follow the file list).
400
+ *
401
+ * @param {object} opts
402
+ * @param {string} opts.file - Starting file
403
+ * @param {number} [opts.hops=2] - Max hops along outgoing IMPORTS edges
404
+ * @param {boolean} [opts.includeDependents=true] - Include files that directly import the starting file
405
+ * @param {number} [opts.maxFiles=15] - Max files to include (direct neighbors always included)
406
+ * @param {boolean} [opts.count=false] - If true, return only counts
407
+ * @param {number} [opts.offset] - Skip first N files in output
408
+ * @param {number} [opts.limit] - Max files to return (after ranking/capping)
409
+ * @returns {{files, symbols, edges, total?}}
410
+ */
411
+ async neighborhood({
412
+ file,
413
+ hops = 2,
414
+ includeDependents = true,
415
+ maxFiles = 15,
416
+ count = false,
417
+ offset,
418
+ limit,
419
+ }) {
420
+ await this._ensureReady();
421
+ const graph = this.cache.getGraph();
422
+ if (!graph || !graph.hasNode(file)) {
423
+ return count
424
+ ? { totalFiles: 0, totalSymbols: 0, totalEdges: 0 }
425
+ : { files: [], symbols: [], edges: [] };
426
+ }
427
+
428
+ // BFS over file nodes, following outgoing IMPORTS edges (dependencies)
429
+ // Track hop depth per file for ranking
430
+ const fileHops = new Map(); // file -> hop distance
431
+ const queue = [{ node: file, depth: 0 }];
432
+ fileHops.set(file, 0);
433
+
434
+ while (queue.length > 0) {
435
+ const { node, depth } = queue.shift();
436
+ if (depth >= hops) continue;
437
+
438
+ graph.forEachOutEdge(node, (edge, attrs, _source, target) => {
439
+ if (attrs.type !== 'IMPORTS') return;
440
+ const targetAttrs = graph.getNodeAttributes(target);
441
+ if (targetAttrs.type !== 'file') return;
442
+ if (!fileHops.has(target)) {
443
+ fileHops.set(target, depth + 1);
444
+ queue.push({ node: target, depth: depth + 1 });
445
+ }
446
+ });
447
+ }
448
+
449
+ // Direct dependents (hop "0.5" — they import the starting file)
450
+ const directDependents = new Set();
451
+ if (includeDependents) {
452
+ graph.forEachInEdge(file, (edge, attrs, source) => {
453
+ if (attrs.type !== 'IMPORTS') return;
454
+ const sourceAttrs = graph.getNodeAttributes(source);
455
+ if (sourceAttrs.type !== 'file') return;
456
+ directDependents.add(source);
457
+ if (!fileHops.has(source)) {
458
+ fileHops.set(source, 1);
459
+ }
460
+ });
461
+ }
462
+
463
+ // Identify direct neighbors (hop 1 deps + direct dependents)
464
+ const directFiles = new Set([file]);
465
+ graph.forEachOutEdge(file, (edge, attrs, _source, target) => {
466
+ if (attrs.type === 'IMPORTS') directFiles.add(target);
467
+ });
468
+ for (const d of directDependents) directFiles.add(d);
469
+
470
+ // Rank all visited files by PageRank, biased toward starting file
471
+ const ranked = this._getRanked([file]);
472
+ const prMap = new Map(ranked);
473
+
474
+ // Score files: direct neighbors get a large bonus, then PageRank
475
+ const allVisited = [...fileHops.keys()];
476
+ const fileScored = allVisited.map(f => {
477
+ const isDirect = directFiles.has(f);
478
+ const hopDist = fileHops.get(f) || 99;
479
+ // Sum PageRank of symbols in this file
480
+ let filePR = 0;
481
+ graph.forEachOutEdge(f, (edge, attrs, _source, target) => {
482
+ if (attrs.type === 'DEFINES') {
483
+ filePR += prMap.get(target) || 0;
484
+ }
485
+ });
486
+ // Direct neighbors always sort first; within each tier, sort by PageRank
487
+ const score = (isDirect ? 1e6 : 0) + filePR * 1e4 - hopDist;
488
+ return { file: f, score, isDirect };
489
+ });
490
+ fileScored.sort((a, b) => b.score - a.score);
491
+
492
+ // Cap: always include direct neighbors, then fill up to maxFiles with ranked hop-2+ files
493
+ const cappedFiles = [];
494
+ const cappedSet = new Set();
495
+ for (const entry of fileScored) {
496
+ if (entry.isDirect || cappedFiles.length < maxFiles) {
497
+ cappedFiles.push(entry.file);
498
+ cappedSet.add(entry.file);
499
+ }
500
+ }
501
+
502
+ // Collect IMPORTS edges involving the starting file only
503
+ const edges = [];
504
+ const edgeSet = new Set();
505
+ graph.forEachOutEdge(file, (edge, attrs, source, target) => {
506
+ if (attrs.type !== 'IMPORTS') return;
507
+ if (!cappedSet.has(target)) return;
508
+ const key = `${source}->${target}`;
509
+ if (!edgeSet.has(key)) {
510
+ edgeSet.add(key);
511
+ edges.push({ source, target, type: 'IMPORTS' });
512
+ }
513
+ });
514
+ graph.forEachInEdge(file, (edge, attrs, source) => {
515
+ if (attrs.type !== 'IMPORTS') return;
516
+ if (!cappedSet.has(source)) return;
517
+ const key = `${source}->${file}`;
518
+ if (!edgeSet.has(key)) {
519
+ edgeSet.add(key);
520
+ edges.push({ source, target: file, type: 'IMPORTS' });
521
+ }
522
+ });
523
+
524
+ // Collect all symbols from capped files, ranked by PageRank
525
+ const symbols = [];
526
+ for (const [symbolKey, _score] of ranked) {
527
+ let attrs;
528
+ try {
529
+ attrs = graph.getNodeAttributes(symbolKey);
530
+ } catch {
531
+ continue;
532
+ }
533
+ if (attrs.type !== 'symbol') continue;
534
+ if (!cappedSet.has(attrs.file)) continue;
535
+
536
+ symbols.push({
537
+ name: attrs.name,
538
+ kind: attrs.kind,
539
+ file: attrs.file,
540
+ lineStart: attrs.lineStart,
541
+ signature: attrs.signature,
542
+ });
543
+ }
544
+
545
+ if (count) {
546
+ return {
547
+ totalFiles: cappedFiles.length,
548
+ totalSymbols: symbols.length,
549
+ totalEdges: edges.length,
550
+ totalVisited: allVisited.length,
551
+ };
552
+ }
553
+
554
+ // Apply pagination to files (symbols follow the file list)
555
+ const { items: paginatedFiles } = paginate(cappedFiles, { offset, limit });
556
+ const paginatedFileSet = new Set(paginatedFiles);
557
+ const paginatedSymbols = symbols.filter(s => paginatedFileSet.has(s.file));
558
+ const paginatedEdges = edges.filter(
559
+ e => paginatedFileSet.has(e.source) || paginatedFileSet.has(e.target)
560
+ );
561
+
562
+ return {
563
+ files: paginatedFiles,
564
+ symbols: paginatedSymbols,
565
+ edges: paginatedEdges,
566
+ total: cappedFiles.length,
567
+ };
568
+ }
569
+
570
+ /**
571
+ * Force a full rebuild.
572
+ */
573
+ async reindex() {
574
+ this._rankedCache = null;
575
+ this._fileScoresCache = null;
576
+ return this.cache.reindex();
577
+ }
578
+
579
+ /**
580
+ * Get index stats.
581
+ */
582
+ async stats() {
583
+ await this._ensureReady();
584
+ const graph = this.cache.getGraph();
585
+ if (!graph) return { files: 0, symbols: 0, edges: 0 };
586
+
587
+ let files = 0;
588
+ let symbols = 0;
589
+ graph.forEachNode((_node, attrs) => {
590
+ if (attrs.type === 'file') files++;
591
+ else if (attrs.type === 'symbol') symbols++;
592
+ });
593
+
594
+ return { files, symbols, edges: graph.size };
595
+ }
596
+ }
597
+
598
+ function formatTree(obj, indent) {
599
+ const lines = [];
600
+ const entries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
601
+
602
+ for (const [key, value] of entries) {
603
+ if (typeof value === 'number') {
604
+ lines.push(`${indent}${key} (${value} symbols)`);
605
+ } else {
606
+ lines.push(`${indent}${key}/`);
607
+ lines.push(formatTree(value, indent + ' '));
608
+ }
609
+ }
610
+
611
+ return lines.join('\n');
612
+ }
613
+
614
+ export { CodeIndex };
615
+ export default CodeIndex;