@mishasinitcyn/betterrank 0.2.8 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mishasinitcyn/betterrank",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/graph.js CHANGED
@@ -25,6 +25,13 @@ const IMPORT_RESOLVE_EXTENSIONS = [
25
25
  '.py', '.rs', '.go', '.rb', '.java',
26
26
  '.c', '.h', '.cpp', '.hpp', '.cc', '.cs', '.php',
27
27
  ];
28
+ const FOCUS_MAX_HOPS = 2;
29
+ const FOCUS_DISTANCE_WEIGHTS = new Map([
30
+ [0, 250],
31
+ [1, 12],
32
+ [2, 2.5],
33
+ ]);
34
+ const FOCUS_DEFAULT_WEIGHT = 0.15;
28
35
 
29
36
  /**
30
37
  * Disambiguate which targets a reference should wire to.
@@ -133,7 +140,14 @@ function wireSymbolReferences(records, graph, defIndex, addedRefs, addedImports)
133
140
  const targets = defIndex.get(ref.name);
134
141
  if (!targets) continue;
135
142
 
136
- const resolvedTargets = disambiguateTargets(targets, file, graph);
143
+ const filteredTargets = ref.kind === 'property'
144
+ ? targets.filter(target => {
145
+ try { return graph.getNodeAttribute(target, 'kind') === 'property'; } catch { return false; }
146
+ })
147
+ : targets;
148
+ if (filteredTargets.length === 0) continue;
149
+
150
+ const resolvedTargets = disambiguateTargets(filteredTargets, file, graph);
137
151
 
138
152
  for (const target of resolvedTargets) {
139
153
  const targetFile = graph.getNodeAttribute(target, 'file');
@@ -263,6 +277,56 @@ function removeFileNodes(graph, filePath) {
263
277
  }
264
278
  }
265
279
 
280
+ function buildFocusDistanceMap(graph, focusFiles, maxHops = FOCUS_MAX_HOPS) {
281
+ const distances = new Map();
282
+ const queue = [];
283
+
284
+ for (const file of focusFiles) {
285
+ if (!graph.hasNode(file)) continue;
286
+ const attrs = graph.getNodeAttributes(file);
287
+ if (attrs.type !== 'file') continue;
288
+ distances.set(file, 0);
289
+ queue.push(file);
290
+ }
291
+
292
+ for (let i = 0; i < queue.length; i++) {
293
+ const current = queue[i];
294
+ const currentDistance = distances.get(current);
295
+ if (currentDistance >= maxHops) continue;
296
+
297
+ const visitNeighbor = neighbor => {
298
+ if (!graph.hasNode(neighbor)) return;
299
+ const neighborAttrs = graph.getNodeAttributes(neighbor);
300
+ if (neighborAttrs.type !== 'file') return;
301
+
302
+ const nextDistance = currentDistance + 1;
303
+ const existing = distances.get(neighbor);
304
+ if (existing !== undefined && existing <= nextDistance) return;
305
+ distances.set(neighbor, nextDistance);
306
+ queue.push(neighbor);
307
+ };
308
+
309
+ graph.forEachOutEdge(current, (_edge, attrs, _source, target) => {
310
+ if (attrs.type !== 'IMPORTS') return;
311
+ visitNeighbor(target);
312
+ });
313
+
314
+ graph.forEachInEdge(current, (_edge, attrs, source) => {
315
+ if (attrs.type !== 'IMPORTS') return;
316
+ visitNeighbor(source);
317
+ });
318
+ }
319
+
320
+ return distances;
321
+ }
322
+
323
+ function getFocusWeight(filePath, focusDistances) {
324
+ if (!focusDistances || focusDistances.size === 0) return 1.0;
325
+ const distance = focusDistances.get(filePath);
326
+ if (distance === undefined) return FOCUS_DEFAULT_WEIGHT;
327
+ return FOCUS_DISTANCE_WEIGHTS.get(distance) || FOCUS_DEFAULT_WEIGHT;
328
+ }
329
+
266
330
  // Path-tier dampening: files outside core source directories get their
267
331
  // PageRank scores multiplied by a fraction. This prevents scripts, tests,
268
332
  // and temp files from dominating the map output over actual source code.
@@ -298,6 +362,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
298
362
  if (graph.order === 0) return [];
299
363
 
300
364
  const g = graph.copy();
365
+ const focusDistances = focusFiles.length > 0 ? buildFocusDistanceMap(graph, focusFiles) : null;
301
366
 
302
367
  if (focusFiles.length > 0) {
303
368
  g.mergeNode('__focus__', { type: 'virtual' });
@@ -329,7 +394,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
329
394
  .map(([key, score]) => {
330
395
  try {
331
396
  const file = graph.getNodeAttribute(key, 'file');
332
- return [key, score * getPathWeight(file, pathTiers)];
397
+ return [key, score * getPathWeight(file, pathTiers) * getFocusWeight(file, focusDistances)];
333
398
  } catch {
334
399
  return [key, score];
335
400
  }
package/src/index.js CHANGED
@@ -579,6 +579,8 @@ class CodeIndex {
579
579
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
580
580
  const callPattern = new RegExp(`(?<![a-zA-Z0-9_])${escaped}\\s*\\(`);
581
581
  const jsxPattern = new RegExp(`<\\s*${escaped}(?=[\\s>/.]|$)`);
582
+ const memberPattern = new RegExp(`\\.\\s*${escaped}\\b`);
583
+ const bracketPattern = new RegExp(`\\[\\s*['"\`]${escaped}['"\`]\\s*\\]`);
582
584
  // Fallback: import/from lines that reference the symbol
583
585
  const importPattern = new RegExp(`(?:import|from)\\s.*\\b${escaped}\\b`);
584
586
 
@@ -605,7 +607,13 @@ class CodeIndex {
605
607
  if (inDef) continue;
606
608
 
607
609
  const line = lines[i];
608
- if (!callPattern.test(line) && !jsxPattern.test(line) && !importPattern.test(line)) continue;
610
+ if (
611
+ !callPattern.test(line) &&
612
+ !jsxPattern.test(line) &&
613
+ !memberPattern.test(line) &&
614
+ !bracketPattern.test(line) &&
615
+ !importPattern.test(line)
616
+ ) continue;
609
617
 
610
618
  const start = Math.max(0, i - context);
611
619
  const end = Math.min(lines.length - 1, i + context);
package/src/parser.js CHANGED
@@ -188,6 +188,7 @@ const REF_QUERIES = {
188
188
  (call_expression function: (identifier) @ref)
189
189
  (import_specifier name: (identifier) @ref)
190
190
  (import_clause (identifier) @ref)
191
+ (member_expression property: (property_identifier) @prop_ref)
191
192
  (jsx_opening_element (identifier) @jsx_ref)
192
193
  (jsx_self_closing_element (identifier) @jsx_ref)
193
194
  (jsx_opening_element (member_expression (identifier) @jsx_ref))
@@ -198,6 +199,7 @@ const REF_QUERIES = {
198
199
  (call_expression function: (identifier) @ref)
199
200
  (import_specifier name: (identifier) @ref)
200
201
  (import_clause (identifier) @ref)
202
+ (member_expression property: (property_identifier) @prop_ref)
201
203
  (type_identifier) @ref
202
204
  `,
203
205
 
@@ -205,6 +207,7 @@ const REF_QUERIES = {
205
207
  (call_expression function: (identifier) @ref)
206
208
  (import_specifier name: (identifier) @ref)
207
209
  (import_clause (identifier) @ref)
210
+ (member_expression property: (property_identifier) @prop_ref)
208
211
  (type_identifier) @ref
209
212
  (jsx_opening_element (identifier) @jsx_ref)
210
213
  (jsx_self_closing_element (identifier) @jsx_ref)
@@ -287,8 +290,12 @@ const KIND_MAP = {
287
290
  };
288
291
 
289
292
  const OUTLINE_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
293
+ const INDEX_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
290
294
  const OUTLINE_BODY_TYPES = new Set(['statement_block', 'class_body', 'object', 'array']);
291
295
  const OUTLINE_CALL_TYPES = new Set(['call_expression', 'new_expression']);
296
+ const SEMANTIC_CONTAINER_NAME_RE = /(router|routes|handlers|actions|reducers|selectors|queries|mutations|registry|registries|map|maps|config|configs|options|endpoints|procedures)$/i;
297
+ const NON_SEMANTIC_CONTAINER_NAME_RE = /(schema|shape|validator|payload|params?|props?|input|output|response|request|dto)$/i;
298
+ const NON_SEMANTIC_CALLEE_NAME_RE = /^(object|array|enum|union|literal|record|tuple|pick|omit|extend|merge|intersection|partial|strictObject|looseObject)$/i;
292
299
 
293
300
  /**
294
301
  * Walk an AST subtree and count node types that reveal structural shape.
@@ -488,6 +495,111 @@ function hasMultilinePairChildren(node) {
488
495
  return false;
489
496
  }
490
497
 
498
+ function isTopLevelVariableDeclarator(node) {
499
+ if (!node || node.type !== 'variable_declarator') return false;
500
+ const decl = node.parent;
501
+ if (!decl || decl.type !== 'lexical_declaration') return false;
502
+ const container = decl.parent;
503
+ return !!container && (container.type === 'program' || container.type === 'export_statement');
504
+ }
505
+
506
+ function extractDeclaratorName(node) {
507
+ const nameNode = node?.childForFieldName('name');
508
+ return nameNode?.type === 'identifier' ? nameNode.text : null;
509
+ }
510
+
511
+ function extractCalleeLeafName(node) {
512
+ if (!node) return null;
513
+
514
+ if (['identifier', 'property_identifier', 'private_property_identifier', 'type_identifier'].includes(node.type)) {
515
+ return node.text;
516
+ }
517
+
518
+ if (node.type === 'member_expression') {
519
+ const propertyNode = node.childForFieldName('property');
520
+ if (propertyNode) return extractCalleeLeafName(propertyNode);
521
+ return extractCalleeLeafName(node.namedChild(node.namedChildCount - 1));
522
+ }
523
+
524
+ return null;
525
+ }
526
+
527
+ function extractCallLikeCalleeName(node) {
528
+ if (!node || (node.type !== 'call_expression' && node.type !== 'new_expression')) return null;
529
+
530
+ const calleeNode = node.childForFieldName('function')
531
+ || node.childForFieldName('constructor')
532
+ || node.namedChild(0);
533
+ return extractCalleeLeafName(calleeNode);
534
+ }
535
+
536
+ function shouldIndexSemanticObjectMembers(node) {
537
+ if (!isTopLevelVariableDeclarator(node)) return false;
538
+
539
+ const variableName = extractDeclaratorName(node);
540
+ const valueNode = node.childForFieldName('value');
541
+ const calleeName = extractCallLikeCalleeName(valueNode);
542
+
543
+ const hasSemanticVariableName = !!variableName
544
+ && SEMANTIC_CONTAINER_NAME_RE.test(variableName)
545
+ && !NON_SEMANTIC_CONTAINER_NAME_RE.test(variableName);
546
+ const hasSemanticCalleeName = !!calleeName && !NON_SEMANTIC_CALLEE_NAME_RE.test(calleeName);
547
+
548
+ if (valueNode?.type === 'object') {
549
+ return hasSemanticVariableName;
550
+ }
551
+
552
+ if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
553
+ return hasSemanticVariableName || hasSemanticCalleeName;
554
+ }
555
+
556
+ return false;
557
+ }
558
+
559
+ function collectDirectPairDefinitions(node, filePath, langName) {
560
+ if (!node || node.type !== 'object') return [];
561
+
562
+ const definitions = [];
563
+ for (let i = 0; i < node.namedChildCount; i++) {
564
+ const child = node.namedChild(i);
565
+ if (child.type !== 'pair') continue;
566
+
567
+ const name = extractPairName(child);
568
+ if (!name) continue;
569
+ if (child.endPosition.row <= child.startPosition.row) continue;
570
+
571
+ definitions.push(createDefinition(name, child, filePath, langName, {
572
+ bodyStartLine: child.startPosition.row + 2,
573
+ kind: 'property',
574
+ }));
575
+ }
576
+
577
+ return definitions;
578
+ }
579
+
580
+ function collectIndexDefinitions(rootNode, filePath, langName) {
581
+ if (!INDEX_EXTRA_LANGUAGES.has(langName)) return [];
582
+
583
+ const definitions = [];
584
+
585
+ function visit(node) {
586
+ if (node.type === 'variable_declarator' && shouldIndexSemanticObjectMembers(node)) {
587
+ const bodyNode = findBodyNode(node);
588
+ if (bodyNode && bodyNode.type === 'object') {
589
+ definitions.push(...collectDirectPairDefinitions(bodyNode, filePath, langName));
590
+ }
591
+ return;
592
+ }
593
+
594
+ for (let i = 0; i < node.namedChildCount; i++) {
595
+ visit(node.namedChild(i));
596
+ }
597
+ }
598
+
599
+ visit(rootNode);
600
+ return definitions;
601
+ }
602
+
491
603
  function collectOutlineDefinitions(rootNode, filePath, langName) {
492
604
  if (!OUTLINE_EXTRA_LANGUAGES.has(langName)) return [];
493
605
 
@@ -618,6 +730,8 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
618
730
  }
619
731
  }
620
732
 
733
+ definitions.push(...collectIndexDefinitions(tree.rootNode, filePath, langName));
734
+
621
735
  if (includeOutlineDefinitions) {
622
736
  definitions.push(...collectOutlineDefinitions(tree.rootNode, filePath, langName));
623
737
  }
@@ -628,11 +742,12 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
628
742
  const refQuery = new Parser.Query(lang, refQueryStr);
629
743
  for (const match of refQuery.matches(tree.rootNode)) {
630
744
  for (const capture of match.captures) {
631
- if (capture.name !== 'ref' && capture.name !== 'jsx_ref') continue;
745
+ if (capture.name !== 'ref' && capture.name !== 'jsx_ref' && capture.name !== 'prop_ref') continue;
632
746
  const name = normalizeReferenceCapture(capture);
633
747
  if (!name) continue;
634
748
  references.push({
635
749
  name,
750
+ kind: capture.name === 'prop_ref' ? 'property' : 'symbol',
636
751
  file: filePath,
637
752
  line: capture.node.startPosition.row + 1,
638
753
  });