codesearch-mcp 0.2.0-alpha → 0.2.2-alpha

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.
@@ -41,6 +41,7 @@ const child_process_1 = require("child_process");
41
41
  const file_utils_1 = require("../file-utils");
42
42
  const MonorepoAwareDiscoveryService_1 = require("./MonorepoAwareDiscoveryService");
43
43
  const legacy_ollama_index_1 = require("./legacy-ollama-index");
44
+ const symbol_name_1 = require("../symbol-name");
44
45
  const SILENT_LOGGER = {
45
46
  debug() { },
46
47
  info() { },
@@ -472,7 +473,22 @@ class SearcherService {
472
473
  async getCallers(symbol, project, calleeFile = null) {
473
474
  const t0 = Date.now();
474
475
  this.logger.debug('SearcherService: getCallers', { symbol, project, calleeFile });
475
- const params = [symbol, project];
476
+ const parsed = (0, symbol_name_1.parseCallerSymbol)(symbol);
477
+ if (!parsed)
478
+ throw new Error('Invalid symbol name');
479
+ const params = [project];
480
+ let nameFilter;
481
+ if (parsed.form === 'bare') {
482
+ params.push(parsed.name);
483
+ nameFilter = `"calleeName" = $${params.length}`;
484
+ }
485
+ else {
486
+ params.push(`${parsed.container}.${parsed.name}`);
487
+ const exactParam = params.length;
488
+ params.push(`%.${(0, symbol_name_1.escapeLike)(parsed.container)}.${(0, symbol_name_1.escapeLike)(parsed.name)}`);
489
+ const suffixParam = params.length;
490
+ nameFilter = `("calleeSymbol" = $${exactParam} OR "calleeSymbol" LIKE $${suffixParam} ESCAPE '\\')`;
491
+ }
476
492
  let fileFilter = '';
477
493
  if (calleeFile) {
478
494
  params.push(calleeFile);
@@ -480,13 +496,32 @@ class SearcherService {
480
496
  }
481
497
  const rows = await this.db.query(`SELECT "fromFile", "fromSymbol", "calleeName", "calleeFile", line
482
498
  FROM code_call
483
- WHERE "calleeName" = $1 AND project = $2
499
+ WHERE project = $1 AND ${nameFilter}
484
500
  ${fileFilter}
485
501
  ORDER BY "fromFile", line
486
502
  LIMIT 200`, params);
487
503
  this.logger.debug('SearcherService: getCallers result', { count: rows.length, ms: Date.now() - t0 });
488
504
  return rows;
489
505
  }
506
+ /**
507
+ * One-line warning when any project in `scope` (the given `project`, or
508
+ * every project with `code_chunk`/`code_symbol` rows) is outdated — shown
509
+ * first in `find_symbol`/`get_callers` output. Any failure to determine the
510
+ * scope or read markers only drops the notice (warn logged); the lookup
511
+ * itself is unaffected.
512
+ */
513
+ async outdatedNotice(project) {
514
+ try {
515
+ const outdated = await this.db.listOutdatedProjects(project ? [project] : null);
516
+ if (outdated.length === 0)
517
+ return null;
518
+ return `⚠ Indexed before member indexing: ${[...outdated].sort().join(', ')} — run rag-index --reindex; methods and qualified names may be missing.`;
519
+ }
520
+ catch (err) {
521
+ this.logger.warn('SearcherService: failed to determine outdated-index scope', { reason: err.message });
522
+ return null;
523
+ }
524
+ }
490
525
  async grepSymbol(symbol, project, excludeFile) {
491
526
  const t0 = Date.now();
492
527
  this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
@@ -499,11 +534,11 @@ class SearcherService {
499
534
  symbol,
500
535
  projectDir,
501
536
  ]);
502
- if (!result) {
537
+ if (!result.ok || result.output === null) {
503
538
  return { matches: [], truncated: false };
504
539
  }
505
540
  const excludeAbsPath = path.join(this.config.workspaceRoot, excludeFile);
506
- const all = result.split('\n').flatMap(line => {
541
+ const all = result.output.split('\n').flatMap(line => {
507
542
  const match = line.match(/^(.+?):(\d+):/);
508
543
  if (!match)
509
544
  return [];
@@ -517,19 +552,29 @@ class SearcherService {
517
552
  return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
518
553
  }
519
554
  // Sync wrapper: validates name (throws synchronously), then returns Promise.
520
- // Methods are never indexed — always resolved via grep. Other kinds use index with grep fallback.
555
+ // Every kind, including 'method', is looked up in the index first.
521
556
  findSymbol(name, project, kind) {
522
- if (!/^[\w$]+$/.test(name))
557
+ const parsed = (0, symbol_name_1.parseFindSymbolName)(name);
558
+ if (!parsed)
523
559
  throw new Error('Invalid symbol name');
524
- if (kind === 'method')
525
- return this.grepFindSymbol(name, project, kind);
526
- return this.findSymbolFromIndex(name, project, kind);
560
+ return this.findSymbolFromIndex(parsed, project, kind);
527
561
  }
528
- async findSymbolFromIndex(name, project, kind) {
562
+ async findSymbolFromIndex(parsed, project, kind) {
529
563
  const t0 = Date.now();
530
- this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'index' });
564
+ this.logger.debug('SearcherService: findSymbol', { parsed, project, kind, source: 'index' });
565
+ const container = 'container' in parsed ? parsed.container : null;
531
566
  try {
532
- const params = [name];
567
+ const params = [parsed.name];
568
+ let nameFilter = 'name = $1';
569
+ let orderPrefix = '';
570
+ if (container !== null) {
571
+ params.push(container);
572
+ const exactParam = params.length;
573
+ params.push(`%.${(0, symbol_name_1.escapeLike)(container)}`);
574
+ const suffixParam = params.length;
575
+ nameFilter += ` AND (container = $${exactParam} OR container LIKE $${suffixParam} ESCAPE '\\')`;
576
+ orderPrefix = `(CASE WHEN container = $${exactParam} THEN 0 ELSE 1 END), `;
577
+ }
533
578
  let projectFilter = '';
534
579
  if (project) {
535
580
  params.push(project);
@@ -541,11 +586,11 @@ class SearcherService {
541
586
  kindFilter = `AND kind = $${params.length}`;
542
587
  }
543
588
  params.push(FIND_SYMBOL_LIMIT + 1);
544
- const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col
545
- FROM code_symbol
546
- WHERE name = $1 ${projectFilter} ${kindFilter}
547
- ORDER BY project, "filePath"
548
- LIMIT $${params.length}`, params);
589
+ const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col, container, "isStatic"
590
+ FROM code_symbol
591
+ WHERE ${nameFilter} ${projectFilter} ${kindFilter}
592
+ ORDER BY ${orderPrefix}project, "filePath", line
593
+ LIMIT $${params.length}`, params);
549
594
  const truncated = rows.length > FIND_SYMBOL_LIMIT;
550
595
  const hits = rows.slice(0, FIND_SYMBOL_LIMIT).map(row => ({
551
596
  filePath: row.filePath,
@@ -553,22 +598,34 @@ class SearcherService {
553
598
  column: row.col ?? 1,
554
599
  kind: row.kind,
555
600
  signature: row.signature ?? row.name,
601
+ qualified: row.container ? `${row.container}.${row.name}` : row.name,
602
+ isStatic: !!row.isStatic,
603
+ source: 'index',
556
604
  }));
557
605
  this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated, ms: Date.now() - t0 });
558
- return { hits, truncated };
606
+ if (hits.length > 0)
607
+ return { hits, truncated, grepFailed: false };
608
+ if (parsed.form === 'quoted' || parsed.form === 'computed')
609
+ return { hits: [], truncated: false, grepFailed: false };
610
+ return this.grepFindSymbol(parsed, project, kind);
559
611
  }
560
- catch {
561
- this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { name, project, ms: Date.now() - t0 });
562
- return this.grepFindSymbol(name, project, kind);
612
+ catch (err) {
613
+ this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { reason: err.message, ms: Date.now() - t0 });
614
+ if (parsed.form === 'quoted' || parsed.form === 'computed')
615
+ return { hits: [], truncated: false, grepFailed: false };
616
+ return this.grepFindSymbol(parsed, project, kind);
563
617
  }
564
618
  }
565
- async grepFindSymbol(name, project, kind) {
619
+ async grepFindSymbol(parsed, project, kind) {
566
620
  const t0 = Date.now();
567
- this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'grep' });
621
+ const grepName = parsed.name; // qualified names are grepped by their last segment, no container filter
622
+ this.logger.debug('SearcherService: findSymbol', { grepName, project, kind, source: 'grep' });
623
+ const pattern = this.buildFindSymbolPattern(grepName, kind);
624
+ if (pattern === null)
625
+ return { hits: [], truncated: false, grepFailed: false };
568
626
  const searchDir = project
569
627
  ? this.resolveProjectRoot(project)
570
628
  : this.config.workspaceRoot;
571
- const pattern = this.buildFindSymbolPattern(name, kind);
572
629
  const result = await this.spawnGrep([
573
630
  '-rn', '-E',
574
631
  '-m', String(FIND_SYMBOL_RAW_LIMIT),
@@ -577,12 +634,14 @@ class SearcherService {
577
634
  pattern,
578
635
  searchDir,
579
636
  ]);
580
- if (!result)
581
- return { hits: [], truncated: false };
582
- const hits = [];
583
- for (const raw of result.split('\n')) {
584
- if (hits.length >= FIND_SYMBOL_LIMIT)
585
- break;
637
+ if (!result.ok) {
638
+ this.logger.warn('SearcherService: findSymbol grep failed', { grepName, project });
639
+ return { hits: [], truncated: false, grepFailed: true };
640
+ }
641
+ if (result.output === null)
642
+ return { hits: [], truncated: false, grepFailed: false };
643
+ const parsedHits = [];
644
+ for (const raw of result.output.split('\n')) {
586
645
  const m = raw.match(/^(.+?):(\d+):(.*)$/);
587
646
  if (!m)
588
647
  continue;
@@ -590,19 +649,23 @@ class SearcherService {
590
649
  const lineNum = parseInt(lineNumStr, 10);
591
650
  if (isNaN(lineNum))
592
651
  continue;
593
- const col = matchedLine.indexOf(name);
652
+ const col = SearcherService.wholeWordIndex(matchedLine, grepName);
594
653
  if (col === -1)
595
654
  continue;
596
- hits.push({
655
+ parsedHits.push({
597
656
  filePath: path.relative(this.config.workspaceRoot, absFile),
598
657
  line: lineNum,
599
658
  column: col + 1,
600
- kind: this.inferKind(matchedLine, name),
659
+ kind: this.inferKind(matchedLine, grepName),
601
660
  signature: matchedLine.trim(),
661
+ qualified: grepName,
662
+ isStatic: false,
663
+ source: 'grep',
602
664
  });
603
665
  }
604
- this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT, ms: Date.now() - t0 });
605
- return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
666
+ const truncated = parsedHits.length > FIND_SYMBOL_LIMIT;
667
+ this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: Math.min(parsedHits.length, FIND_SYMBOL_LIMIT), truncated, ms: Date.now() - t0 });
668
+ return { hits: parsedHits.slice(0, FIND_SYMBOL_LIMIT), truncated, grepFailed: false };
606
669
  }
607
670
  listFiles(relativePath, project, depth, pattern) {
608
671
  const t0 = Date.now();
@@ -740,36 +803,67 @@ class SearcherService {
740
803
  }
741
804
  }
742
805
  }
806
+ /**
807
+ * ERE pattern for a grep-fallback kind, or null when that kind is never
808
+ * grepped (`function_property`/`method_signature` are index-only; a bare
809
+ * `constructor` search only makes sense when the name itself is `constructor`).
810
+ */
743
811
  buildFindSymbolPattern(name, kind) {
812
+ if (kind === 'function_property' || kind === 'method_signature')
813
+ return null;
814
+ if (kind === 'constructor')
815
+ return name === 'constructor' ? `(^|[[:space:]])constructor[[:space:]]*\\(` : null;
816
+ const esc = name.replace(/\$/g, '\\$'); // ERE end-of-line metachar
817
+ const MODIFIER = '(public|private|protected|static|abstract|async|override)';
818
+ const BOUNDARY = '(^|[^[:alnum:]_$])';
744
819
  const patterns = {
745
- function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${name}[[:space:](<]`,
746
- class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${name}[[:space:]{<]`,
747
- type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
748
- interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
749
- variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
750
- method: `(public[[:space:]]+|async[[:space:]]+|static[[:space:]]+|override[[:space:]]+)+(async[[:space:]]+)?${name}[[:space:]]*[(<]`,
820
+ function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${esc}[[:space:](<]`,
821
+ class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${esc}[[:space:]{<]`,
822
+ type: `(export[[:space:]]+)?type[[:space:]]+${esc}[[:space:]=<]`,
823
+ interface: `(export[[:space:]]+)?interface[[:space:]]+${esc}[[:space:]{<]`,
824
+ variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${esc}[[:space:]=:]`,
825
+ method: `${BOUNDARY}(${MODIFIER}[[:space:]]+)+\\*?[[:space:]]*${esc}[[:space:]]*[(<]`,
826
+ get_accessor: `(^|[[:space:]])get[[:space:]]+${esc}[[:space:]]*\\(`,
827
+ set_accessor: `(^|[[:space:]])set[[:space:]]+${esc}[[:space:]]*\\(`,
751
828
  };
752
829
  if (kind === 'all')
753
830
  return Object.values(patterns).join('|');
754
831
  return patterns[kind];
755
832
  }
833
+ /** 1-based index of the first whole-word occurrence of `name` in `line`, or -1. */
834
+ static wholeWordIndex(line, name) {
835
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
836
+ const re = new RegExp(`(?<![\\w$#])${escaped}(?![\\w$#])`);
837
+ const m = re.exec(line);
838
+ return m ? m.index : -1;
839
+ }
756
840
  inferKind(line, name) {
757
- const before = line.slice(0, line.indexOf(name));
758
- if (/\bfunction\s+$/.test(before))
841
+ if (name === 'constructor')
842
+ return 'constructor';
843
+ const idx = SearcherService.wholeWordIndex(line, name);
844
+ const before = idx === -1 ? line : line.slice(0, idx);
845
+ const tokens = before.trim().split(/\s+/).filter(Boolean);
846
+ const last = (tokens[tokens.length - 1] ?? '').replace(/\*$/, '');
847
+ if (last === 'get')
848
+ return 'get_accessor';
849
+ if (last === 'set')
850
+ return 'set_accessor';
851
+ if (last === 'function')
759
852
  return 'function';
760
- if (/\bclass\s+$/.test(before))
853
+ if (last === 'class')
761
854
  return 'class';
762
- if (/\binterface\s+$/.test(before))
855
+ if (last === 'interface')
763
856
  return 'interface';
764
- if (/\btype\s+$/.test(before))
857
+ if (last === 'type')
765
858
  return 'type';
766
- if (/\b(public|async|static|override)\s+$/.test(before))
859
+ if (['public', 'private', 'protected', 'static', 'abstract', 'async', 'override'].includes(last))
767
860
  return 'method';
768
861
  return 'variable';
769
862
  }
770
863
  /**
771
- * Async wrapper around grep to avoid blocking the event loop.
772
- * Returns stdout as a string, or null if grep returned no results or errored.
864
+ * Async wrapper around grep to avoid blocking the event loop. `ok: false`
865
+ * means grep could not be spawned or exited with status ≥ 2 (distinguishable
866
+ * from a clean "no matches", which is `ok: true, output: null`).
773
867
  */
774
868
  spawnGrep(args) {
775
869
  return new Promise((resolve) => {
@@ -786,19 +880,16 @@ class SearcherService {
786
880
  proc.stderr.on('data', () => { });
787
881
  proc.on('error', (err) => {
788
882
  this.logger.error('SearcherService: grep spawn failed', { reason: err.message });
789
- resolve(null);
883
+ resolve({ ok: false });
790
884
  });
791
885
  proc.on('close', (code) => {
792
- // grep returns 1 for "no matches" — not an error
793
- if (code !== 0 && code !== 1) {
886
+ // grep returns 1 for "no matches" — not an error; 2+ is a real failure
887
+ if (code !== null && code >= 2) {
794
888
  this.logger.warn('SearcherService: grep exited with code', { code });
889
+ resolve({ ok: false });
890
+ return;
795
891
  }
796
- if (chunks.length === 0) {
797
- resolve(null);
798
- }
799
- else {
800
- resolve(Buffer.concat(chunks).toString('utf-8'));
801
- }
892
+ resolve({ ok: true, output: chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8') });
802
893
  });
803
894
  });
804
895
  }
@@ -0,0 +1,44 @@
1
+ export declare const BARE_NAME_RE: RegExp;
2
+ export declare const QUALIFIED_NAME_RE: RegExp;
3
+ export type FindSymbolQuery = {
4
+ form: 'bare';
5
+ name: string;
6
+ } | {
7
+ form: 'qualified';
8
+ container: string;
9
+ name: string;
10
+ } | {
11
+ form: 'quoted';
12
+ container: string | null;
13
+ name: string;
14
+ } | {
15
+ form: 'computed';
16
+ container: string | null;
17
+ name: string;
18
+ };
19
+ /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
20
+ export declare function splitQualified(input: string): {
21
+ container: string;
22
+ name: string;
23
+ };
24
+ /**
25
+ * Parse a `find_symbol` name into one of the four accepted forms (bare,
26
+ * qualified, quoted, computed), or null when it matches none — the caller
27
+ * should reject with `Invalid symbol name` without querying anything.
28
+ */
29
+ export declare function parseFindSymbolName(input: string): FindSymbolQuery | null;
30
+ /**
31
+ * Parse a `get_callers` symbol into bare or qualified form only — the quoted
32
+ * and computed forms of `find_symbol` are meaningless here (calls through
33
+ * element access are never recorded in the call graph).
34
+ */
35
+ export declare function parseCallerSymbol(input: string): {
36
+ form: 'bare';
37
+ name: string;
38
+ } | {
39
+ form: 'qualified';
40
+ container: string;
41
+ name: string;
42
+ } | null;
43
+ /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
44
+ export declare function escapeLike(s: string): string;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QUALIFIED_NAME_RE = exports.BARE_NAME_RE = void 0;
4
+ exports.splitQualified = splitQualified;
5
+ exports.parseFindSymbolName = parseFindSymbolName;
6
+ exports.parseCallerSymbol = parseCallerSymbol;
7
+ exports.escapeLike = escapeLike;
8
+ exports.BARE_NAME_RE = /^#?[\w$]+$/;
9
+ exports.QUALIFIED_NAME_RE = /^#?[\w$]+(\.#?[\w$]+)+$/;
10
+ const PFX_SRC = '(?:#?[\\w$]+(?:\\.#?[\\w$]+)*\\.)?';
11
+ const QUOTED_RE = new RegExp(`^(${PFX_SRC})('[^'\\r\\n]+'|"[^"\\r\\n]+")$`);
12
+ const COMPUTED_RE = new RegExp(`^(${PFX_SRC})(\\[[^\\]\\r\\n]+\\])$`);
13
+ const MAX_QUOTED_COMPUTED_LEN = 256;
14
+ /** Strip a qualified name's trailing `.` container prefix, if present. */
15
+ function stripTrailingDot(prefix) {
16
+ if (!prefix)
17
+ return null;
18
+ return prefix.slice(0, -1);
19
+ }
20
+ /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
21
+ function splitQualified(input) {
22
+ const idx = input.lastIndexOf('.');
23
+ return { container: input.slice(0, idx), name: input.slice(idx + 1) };
24
+ }
25
+ /**
26
+ * Parse a `find_symbol` name into one of the four accepted forms (bare,
27
+ * qualified, quoted, computed), or null when it matches none — the caller
28
+ * should reject with `Invalid symbol name` without querying anything.
29
+ */
30
+ function parseFindSymbolName(input) {
31
+ if (input.length <= MAX_QUOTED_COMPUTED_LEN) {
32
+ const quoted = input.match(QUOTED_RE);
33
+ if (quoted) {
34
+ return { form: 'quoted', container: stripTrailingDot(quoted[1]), name: quoted[2].slice(1, -1) };
35
+ }
36
+ const computed = input.match(COMPUTED_RE);
37
+ if (computed) {
38
+ return { form: 'computed', container: stripTrailingDot(computed[1]), name: computed[2].replace(/\s+/g, '') };
39
+ }
40
+ }
41
+ if (exports.QUALIFIED_NAME_RE.test(input)) {
42
+ const { container, name } = splitQualified(input);
43
+ return { form: 'qualified', container, name };
44
+ }
45
+ if (exports.BARE_NAME_RE.test(input)) {
46
+ return { form: 'bare', name: input };
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Parse a `get_callers` symbol into bare or qualified form only — the quoted
52
+ * and computed forms of `find_symbol` are meaningless here (calls through
53
+ * element access are never recorded in the call graph).
54
+ */
55
+ function parseCallerSymbol(input) {
56
+ if (exports.QUALIFIED_NAME_RE.test(input)) {
57
+ const { container, name } = splitQualified(input);
58
+ return { form: 'qualified', container, name };
59
+ }
60
+ if (exports.BARE_NAME_RE.test(input)) {
61
+ return { form: 'bare', name: input };
62
+ }
63
+ return null;
64
+ }
65
+ /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
66
+ function escapeLike(s) {
67
+ return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { SymbolEntry } from './interfaces/IAstParser';
2
+ export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
3
+ /**
4
+ * Combine top-level symbols and members into the rows written to `code_symbol`.
5
+ * Overload signatures of one declaration — same `(container, name, kind,
6
+ * isStatic)` — collapse to a single representative row: the one with a body
7
+ * (the implementation) when present, otherwise the earliest by line (the first
8
+ * signature in source order). Static and instance members sharing a name are
9
+ * never collapsed into each other (`isStatic` is part of the group key). A
10
+ * final dedup by `(container, name, kind, line)` matches the DB's unique index.
11
+ */
12
+ export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSymbolRows = buildSymbolRows;
4
+ /**
5
+ * Combine top-level symbols and members into the rows written to `code_symbol`.
6
+ * Overload signatures of one declaration — same `(container, name, kind,
7
+ * isStatic)` — collapse to a single representative row: the one with a body
8
+ * (the implementation) when present, otherwise the earliest by line (the first
9
+ * signature in source order). Static and instance members sharing a name are
10
+ * never collapsed into each other (`isStatic` is part of the group key). A
11
+ * final dedup by `(container, name, kind, line)` matches the DB's unique index.
12
+ */
13
+ function buildSymbolRows(symbols, members) {
14
+ const groups = new Map();
15
+ for (const entry of [...symbols, ...members]) {
16
+ const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
17
+ const group = groups.get(key);
18
+ if (group)
19
+ group.push(entry);
20
+ else
21
+ groups.set(key, [entry]);
22
+ }
23
+ const collapsed = [];
24
+ for (const group of groups.values()) {
25
+ if (group.length === 1) {
26
+ collapsed.push(group[0]);
27
+ continue;
28
+ }
29
+ const implementation = group.find(e => e.hasBody);
30
+ collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
31
+ }
32
+ const dedup = new Map();
33
+ for (const entry of collapsed) {
34
+ const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
35
+ dedup.set(key, entry);
36
+ }
37
+ return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
38
+ }
@@ -2,7 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FindSymbolTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
- const VALID_KINDS = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
5
+ const VALID_KINDS = [
6
+ 'function', 'class', 'type', 'interface', 'variable',
7
+ 'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
8
+ 'all',
9
+ ];
6
10
  class FindSymbolTool {
7
11
  searcher;
8
12
  constructor(searcher) {
@@ -11,11 +15,16 @@ class FindSymbolTool {
11
15
  definition() {
12
16
  return {
13
17
  name: 'find_symbol',
14
- description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables, methods) by exact name. Use when you know the symbol name and want its definition location fast.',
18
+ description: 'Find TypeScript/JavaScript symbol definitions by exact name: top-level declarations (functions, classes, types, ' +
19
+ 'interfaces, variables), class/object-literal members (methods, get/set accessors, constructors, function ' +
20
+ 'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
21
+ '(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
22
+ "(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
23
+ 'want its definition location fast.',
15
24
  inputSchema: {
16
25
  type: 'object',
17
26
  properties: {
18
- name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
27
+ name: { type: 'string', description: 'Bare, qualified (Class.method), quoted, or computed symbol name (case-sensitive)' },
19
28
  project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
20
29
  kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
21
30
  },
@@ -28,15 +37,29 @@ class FindSymbolTool {
28
37
  const project = args.project ? String(args.project) : null;
29
38
  const rawKind = args.kind;
30
39
  const kind = VALID_KINDS.includes(rawKind) ? rawKind : 'all';
31
- const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
32
- if (hits.length === 0) {
33
- return (0, format_helpers_1.textResponse)(`No definitions found for symbol: ${name}`);
40
+ let parsed;
41
+ try {
42
+ parsed = await this.searcher.findSymbol(name, project, kind);
43
+ }
44
+ catch (err) {
45
+ return (0, format_helpers_1.errorResponse)(err.message);
34
46
  }
47
+ const { hits, truncated, grepFailed } = parsed;
48
+ const notice = await this.searcher.outdatedNotice(project);
35
49
  const lines = [];
50
+ if (notice)
51
+ lines.push(notice + '\n');
52
+ if (hits.length === 0) {
53
+ const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
54
+ lines.push(`No definitions found for symbol: ${name}${suffix}`);
55
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
56
+ }
36
57
  if (truncated)
37
58
  lines.push(`*(truncated to ${hits.length} results)*\n`);
38
59
  for (const h of hits) {
39
- lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
60
+ const staticTag = h.isStatic ? ' static' : '';
61
+ const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
62
+ lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
40
63
  lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
41
64
  }
42
65
  return (0, format_helpers_1.textResponse)(lines.join('\n'));
@@ -10,11 +10,15 @@ class GetCallersTool {
10
10
  definition() {
11
11
  return {
12
12
  name: 'get_callers',
13
- description: 'Find all call sites of a function or method by exact name. Uses the call graph index — faster and more precise than grep. Use after find_symbol to trace where a symbol is actually called. Returns empty if rag-index backfill --calls has not been run.',
13
+ description: 'Find all call sites of a function or method by exact name. Accepts a bare name (every call site with that ' +
14
+ 'callee name) or a qualified name (`Class.method`, from find_symbol output) to disambiguate same-named methods ' +
15
+ 'of different classes. Uses the call graph index — faster and more precise than grep. A call through an ' +
16
+ 'interface-typed receiver is recorded under the interface member (e.g. `IEmbedder.embedBatch`), not the ' +
17
+ 'implementing class. Use after find_symbol to trace where a symbol is actually called.',
14
18
  inputSchema: {
15
19
  type: 'object',
16
20
  properties: {
17
- symbol: { type: 'string', description: 'Exact function or method name (case-sensitive)' },
21
+ symbol: { type: 'string', description: 'Bare or qualified (Class.method) function/method name (case-sensitive)' },
18
22
  project: { type: 'string', description: 'Project name (from list_projects)' },
19
23
  callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
20
24
  },
@@ -26,11 +30,22 @@ class GetCallersTool {
26
30
  const symbol = String(args.symbol ?? '');
27
31
  const project = String(args.project ?? '');
28
32
  const calleeFile = args.callee_file ? String(args.callee_file) : null;
29
- const rows = await this.searcher.getCallers(symbol, project, calleeFile);
33
+ let rows;
34
+ try {
35
+ rows = await this.searcher.getCallers(symbol, project, calleeFile);
36
+ }
37
+ catch (err) {
38
+ return (0, format_helpers_1.errorResponse)(err.message);
39
+ }
40
+ const notice = await this.searcher.outdatedNotice(project);
41
+ const lines = [];
42
+ if (notice)
43
+ lines.push(notice + '\n');
30
44
  if (rows.length === 0) {
31
- return (0, format_helpers_1.textResponse)(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index backfill --calls\` if the call graph has not been populated yet.`);
45
+ lines.push(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index --reindex\` if the call graph has not been populated yet.`);
46
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
32
47
  }
33
- const lines = [`**Call sites of \`${symbol}\`** (${rows.length} found)\n`];
48
+ lines.push(`**Call sites of \`${symbol}\`** (${rows.length} found)\n`);
34
49
  for (const r of rows) {
35
50
  const loc = r.line != null ? ` line ${r.line}` : '';
36
51
  const ctx = r.fromSymbol ? `, in \`${r.fromSymbol}\`` : '';