@aiderdesk/tree-sitter-utils 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -97,6 +97,7 @@ __export(index_exports, {
97
97
  CacheManager: () => CacheManager,
98
98
  GraphBuilder: () => GraphBuilder,
99
99
  PageRankCalculator: () => PageRankCalculator,
100
+ RepoMapService: () => RepoMapService,
100
101
  SymbolExtractor: () => SymbolExtractor,
101
102
  TreeRenderer: () => TreeRenderer,
102
103
  TreeSitterAnalyzer: () => TreeSitterAnalyzer,
@@ -116,6 +117,9 @@ __export(index_exports, {
116
117
  });
117
118
  module.exports = __toCommonJS(index_exports);
118
119
 
120
+ // src/tree-sitter-analyzer.ts
121
+ var path9 = __toESM(require("path"));
122
+
119
123
  // src/tree-sitter-parser.ts
120
124
  var fs2 = __toESM(require("fs/promises"));
121
125
  var path4 = __toESM(require("path"));
@@ -210,8 +214,8 @@ var LANGUAGE_WASM_MAP = {
210
214
  file: "tree-sitter-ocaml_interface.wasm"
211
215
  },
212
216
  c_sharp: {
213
- url: "https://unpkg.com/tree-sitter-c-sharp/tree-sitter-c_sharp.wasm",
214
- file: "tree-sitter-c_sharp.wasm"
217
+ url: "https://unpkg.com/tree-sitter-c-sharp@0.23.5/tree-sitter-c_sharp.wasm",
218
+ file: "tree-sitter-c_sharp-0.23.5.wasm"
215
219
  },
216
220
  haskell: {
217
221
  url: "https://unpkg.com/tree-sitter-haskell/tree-sitter-haskell.wasm",
@@ -223,11 +227,11 @@ var LANGUAGE_WASM_MAP = {
223
227
  }
224
228
  };
225
229
  function getCacheDir() {
226
- const platform3 = os.platform();
230
+ const platform2 = os.platform();
227
231
  const homeDir = os.homedir();
228
- if (platform3 === "darwin") {
232
+ if (platform2 === "darwin") {
229
233
  return path2.join(homeDir, "Library", "Caches", "aiderdesk", "tree-sitter");
230
- } else if (platform3 === "win32") {
234
+ } else if (platform2 === "win32") {
231
235
  return path2.join(process.env.APPDATA || path2.join(homeDir, "AppData", "Roaming"), "aiderdesk", "tree-sitter");
232
236
  } else {
233
237
  return path2.join(homeDir, ".cache", "aiderdesk", "tree-sitter");
@@ -286,175 +290,179 @@ var QUERIES_DIR = path3.join(RESOURCES_DIR, "queries");
286
290
  var WASM_DIR = path3.join(RESOURCES_DIR, "wasm");
287
291
 
288
292
  // src/tree-sitter-parser.ts
293
+ var runtimeInitialization;
289
294
  var TreeSitterParser = class {
290
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
291
295
  parser = null;
292
296
  languages = /* @__PURE__ */ new Map();
293
- queries = /* @__PURE__ */ new Map();
294
- initialized = false;
295
- async initialize() {
296
- if (this.initialized) {
297
- return;
298
- }
299
- try {
300
- await import_web_tree_sitter.Parser.init();
301
- this.parser = new import_web_tree_sitter.Parser();
302
- try {
303
- const queryFiles = await fs2.readdir(QUERIES_DIR);
304
- for (const file of queryFiles) {
305
- if (file.endsWith("-tags.scm")) {
306
- const language = file.replace("-tags.scm", "");
307
- const queryPath = path4.join(QUERIES_DIR, file);
308
- const queryContent = await fs2.readFile(queryPath, "utf-8");
309
- this.queries.set(language, queryContent);
297
+ initialization;
298
+ queue = Promise.resolve();
299
+ initialize() {
300
+ this.initialization ??= this.initializeParser();
301
+ return this.initialization;
302
+ }
303
+ async initializeParser() {
304
+ runtimeInitialization ??= import_web_tree_sitter.Parser.init();
305
+ await runtimeInitialization;
306
+ this.parser = new import_web_tree_sitter.Parser();
307
+ }
308
+ loadLanguage(name) {
309
+ let pending = this.languages.get(name);
310
+ if (!pending) {
311
+ pending = (async () => {
312
+ const buffer = await downloadWasmForLanguage(name);
313
+ if (!buffer) {
314
+ throw new Error(`No WASM available for ${name}`);
315
+ }
316
+ const language = await import_web_tree_sitter.Language.load(buffer);
317
+ const source = await fs2.readFile(path4.join(QUERIES_DIR, `${name}-tags.scm`), "utf-8");
318
+ const query = new import_web_tree_sitter.Query(language, source);
319
+ if (name === "c_sharp") {
320
+ for (const capture of query.captureNames) {
321
+ if (!capture.startsWith("name.")) query.disableCapture(capture);
310
322
  }
311
323
  }
312
- } catch (error) {
313
- logger_default.warn("[TreeSitterParser] Could not load query files from resources:", error);
314
- }
315
- this.initialized = true;
316
- logger_default.info(`[TreeSitterParser] Initialized with ${this.queries.size} language queries`);
317
- } catch (error) {
318
- logger_default.error("[TreeSitterParser] Initialization failed:", error);
319
- throw error;
324
+ return { language, query };
325
+ })();
326
+ this.languages.set(name, pending);
327
+ void pending.catch(() => this.languages.delete(name));
320
328
  }
329
+ return pending;
321
330
  }
322
- async loadLanguage(language) {
323
- if (this.languages.has(language)) {
324
- return this.languages.get(language);
325
- }
326
- try {
327
- const wasmBuffer = await downloadWasmForLanguage(language);
328
- if (!wasmBuffer) {
329
- return null;
330
- }
331
- const lang = await import_web_tree_sitter.Language.load(wasmBuffer);
332
- this.languages.set(language, lang);
333
- return lang;
334
- } catch (error) {
335
- logger_default.error(`[TreeSitterParser] Failed to load language ${language}:`, error);
336
- return null;
337
- }
331
+ parseFile(filePath, rootDir, compactReferences = false) {
332
+ const result = this.queue.then(() => this.parse(filePath, rootDir, compactReferences));
333
+ this.queue = result.catch(() => void 0);
334
+ return result;
338
335
  }
339
- async parseFile(filePath, rootDir) {
340
- if (!this.initialized || !this.parser) {
341
- await this.initialize();
342
- }
343
- if (!this.parser) {
336
+ async parse(filePath, rootDir, compactReferences = false) {
337
+ await this.initialize();
338
+ const name = detectLanguage(filePath);
339
+ if (!name || !this.parser) {
344
340
  return [];
345
341
  }
346
- const language = detectLanguage(filePath);
347
- if (!language) {
348
- return [];
349
- }
350
- const lang = await this.loadLanguage(language);
351
- if (!lang) {
352
- return [];
353
- }
354
- const query = this.queries.get(language);
355
- if (!query) {
356
- return [];
342
+ const { language, query } = await this.loadLanguage(name);
343
+ const source = await fs2.readFile(filePath, "utf-8");
344
+ this.parser.setLanguage(language);
345
+ const tree = this.parser.parse(source);
346
+ if (!tree) {
347
+ throw new Error(`Unable to parse ${filePath}`);
357
348
  }
358
349
  try {
359
- const sourceCode = await fs2.readFile(filePath, "utf-8");
360
- this.parser.setLanguage(lang);
361
- const tree = this.parser.parse(sourceCode);
362
- if (!tree) {
363
- return [];
364
- }
365
- const tags = this.extractTags(tree.rootNode, query, filePath, lang, rootDir);
366
- return tags;
367
- } catch (error) {
368
- logger_default.error(`[TreeSitterParser] Error parsing file ${filePath}:`, error);
369
- return [];
370
- }
371
- }
372
- extractTags(node, query, filePath, language, rootDir) {
373
- const tags = [];
374
- try {
375
- const queryObj = new import_web_tree_sitter.Query(language, query);
376
- const matches = queryObj.matches(node);
377
- logger_default.debug(`[TreeSitterParser] Found ${matches.length} matches in ${filePath}`);
378
- const relFname = rootDir ? path4.relative(rootDir, filePath) : filePath;
379
- for (const match of matches) {
350
+ const tags = [];
351
+ const references = /* @__PURE__ */ new Map();
352
+ const relative5 = rootDir ? path4.relative(rootDir, filePath).replaceAll(path4.sep, "/") : filePath;
353
+ for (const match of query.matches(tree.rootNode)) {
380
354
  for (const capture of match.captures) {
381
- if (!capture.name.startsWith("name.")) {
382
- continue;
355
+ if (capture.name.startsWith("name.")) {
356
+ const kind = capture.name.includes("definition") ? "def" : "ref";
357
+ const name2 = capture.node.text;
358
+ const previous = compactReferences && kind === "ref" ? references.get(name2) : void 0;
359
+ if (previous) {
360
+ previous.count = (previous.count ?? 1) + 1;
361
+ continue;
362
+ }
363
+ const tag = {
364
+ rel_fname: relative5,
365
+ fname: filePath,
366
+ line: capture.node.startPosition.row,
367
+ // Detach sliced node text so cached tags cannot retain entire source files.
368
+ name: Buffer.from(name2, "utf8").toString("utf8"),
369
+ kind
370
+ };
371
+ tags.push(tag);
372
+ if (compactReferences && kind === "ref") references.set(tag.name, tag);
383
373
  }
384
- const name = capture.node.text;
385
- const kind = this.determineKind(capture.name);
386
- const line = capture.node.startPosition.row;
387
- logger_default.debug(`[TreeSitterParser] Found tag: ${name} (${kind}) at line ${line}`);
388
- tags.push({
389
- rel_fname: relFname,
390
- fname: filePath,
391
- line,
392
- name,
393
- kind
394
- });
395
374
  }
396
375
  }
397
- } catch (error) {
398
- logger_default.error(`[TreeSitterParser] Error extracting tags from ${filePath}:`, error);
376
+ return tags;
377
+ } finally {
378
+ tree.delete();
399
379
  }
400
- return tags;
401
380
  }
402
- determineKind(captureName) {
403
- if (captureName.includes("definition")) {
404
- return "def";
381
+ async dispose() {
382
+ await this.queue;
383
+ for (const pending of this.languages.values()) {
384
+ try {
385
+ const { query } = await pending;
386
+ query.delete();
387
+ } catch {
388
+ }
405
389
  }
406
- return "ref";
390
+ this.languages.clear();
391
+ this.parser?.delete();
392
+ this.parser = null;
393
+ this.initialization = void 0;
407
394
  }
408
395
  };
409
396
 
410
397
  // src/symbol-extractor.ts
398
+ var fs3 = __toESM(require("fs/promises"));
399
+ var path5 = __toESM(require("path"));
411
400
  init_language_detector();
412
401
  var SymbolExtractor = class {
413
402
  constructor(parser) {
414
403
  this.parser = parser;
415
404
  }
416
405
  cacheManager = null;
406
+ memory = /* @__PURE__ */ new Map();
407
+ lastRoot;
408
+ lastFiles = "";
409
+ revision = 0;
417
410
  setCacheManager(cacheManager) {
418
411
  this.cacheManager = cacheManager;
419
412
  }
420
- async extractTags(files, rootDir) {
413
+ async extractTags(files, rootDir, signal, compactReferences = false) {
421
414
  const allTags = [];
422
- const supportedFiles = files.filter(isLanguageSupported);
423
- logger_default.info(`[SymbolExtractor] Extracting tags from ${supportedFiles.length} supported files out of ${files.length} total`);
424
- const batchSize = 50;
425
- for (let i = 0; i < supportedFiles.length; i += batchSize) {
426
- const batch = supportedFiles.slice(i, i + batchSize);
427
- const batchTags = await Promise.all(
428
- batch.map(async (file) => {
415
+ const supported = files.filter(isLanguageSupported);
416
+ const fileKey = JSON.stringify([compactReferences, supported]);
417
+ if (this.lastRoot !== rootDir || this.lastFiles !== fileKey) {
418
+ this.revision++;
419
+ this.memory.clear();
420
+ this.lastRoot = rootDir;
421
+ this.lastFiles = fileKey;
422
+ }
423
+ for (let i = 0; i < supported.length; i += 50) {
424
+ signal?.throwIfAborted();
425
+ const results = await Promise.all(
426
+ supported.slice(i, i + 50).map(async (file) => {
429
427
  try {
430
- if (this.cacheManager) {
431
- const currentMtime = await this.cacheManager.getFileMtime(file);
432
- if (currentMtime !== null) {
433
- const cached = await this.cacheManager.getFileCache(file);
434
- if (cached && cached.mtime === currentMtime) {
435
- logger_default.debug(`[SymbolExtractor] Cache hit for ${file}`);
436
- if (rootDir) {
437
- const path7 = await import("path");
438
- const relPath = path7.relative(rootDir, file);
439
- return cached.tags.map((tag) => ({ ...tag, rel_fname: relPath }));
440
- }
441
- return cached.tags;
442
- }
443
- const tags = await this.parser.parseFile(file, rootDir);
444
- await this.cacheManager.setFileCache(file, currentMtime, tags);
428
+ const before = await fs3.stat(file);
429
+ const fingerprint = `${compactReferences}:${before.mtimeMs}:${before.ctimeMs}:${before.size}`;
430
+ const memory = this.memory.get(file);
431
+ if (this.cacheManager && memory?.fingerprint === fingerprint) {
432
+ return memory.tags;
433
+ }
434
+ this.revision++;
435
+ const cached = await this.cacheManager?.getFileCache(file);
436
+ let tags;
437
+ if (cached?.fingerprint === fingerprint) {
438
+ const relative5 = rootDir ? path5.relative(rootDir, file).replaceAll(path5.sep, "/") : file;
439
+ tags = cached.tags.map((tag) => ({
440
+ ...tag,
441
+ rel_fname: relative5
442
+ }));
443
+ } else {
444
+ tags = await this.parser.parseFile(file, rootDir, compactReferences);
445
+ const after = await fs3.stat(file);
446
+ if (`${compactReferences}:${after.mtimeMs}:${after.ctimeMs}:${after.size}` !== fingerprint) {
445
447
  return tags;
446
448
  }
449
+ await this.cacheManager?.setFileCache(file, before.mtimeMs, tags, fingerprint);
447
450
  }
448
- return await this.parser.parseFile(file, rootDir);
451
+ this.memory.set(file, { fingerprint, tags });
452
+ return tags;
449
453
  } catch (error) {
454
+ this.revision++;
455
+ this.memory.delete(file);
450
456
  logger_default.error(`[SymbolExtractor] Error processing ${file}:`, error);
451
457
  return [];
452
458
  }
453
459
  })
454
460
  );
455
- allTags.push(...batchTags.flat());
461
+ for (const tags of results) {
462
+ for (const tag of tags) allTags.push(tag);
463
+ }
456
464
  }
457
- logger_default.info(`[SymbolExtractor] Extracted ${allTags.length} tags total`);
465
+ signal?.throwIfAborted();
458
466
  return allTags;
459
467
  }
460
468
  filterDefinitions(tags) {
@@ -474,163 +482,93 @@ var SymbolExtractor = class {
474
482
  }
475
483
  };
476
484
 
477
- // src/graph-builder.ts
478
- var import_graphology = __toESM(require("graphology"));
479
- var GraphBuilder = class {
480
- buildDependencyGraph(tags) {
481
- const graph = new import_graphology.default({ type: "directed", allowSelfLoops: false, multi: true });
482
- const defines = /* @__PURE__ */ new Map();
483
- const references = /* @__PURE__ */ new Map();
485
+ // src/dependency-index.ts
486
+ var DependencyIndex = class {
487
+ definitions = [];
488
+ relations = [];
489
+ outbound = /* @__PURE__ */ new Map();
490
+ constructor(tags) {
484
491
  const definitions = /* @__PURE__ */ new Map();
492
+ const references = /* @__PURE__ */ new Map();
485
493
  for (const tag of tags) {
486
494
  if (tag.kind === "def") {
487
- if (!defines.has(tag.name)) {
488
- defines.set(tag.name, /* @__PURE__ */ new Set());
489
- }
490
- defines.get(tag.name).add(tag.rel_fname);
491
- const key = `${tag.rel_fname}:${tag.name}`;
492
- if (!definitions.has(key)) {
493
- definitions.set(key, []);
494
- }
495
- definitions.get(key).push(tag);
496
- } else if (tag.kind === "ref") {
497
- if (!references.has(tag.name)) {
498
- references.set(tag.name, []);
499
- }
500
- references.get(tag.name).push(tag.rel_fname);
495
+ this.definitions.push(tag);
496
+ let files = definitions.get(tag.name);
497
+ if (!files) definitions.set(tag.name, files = /* @__PURE__ */ new Set());
498
+ files.add(tag.rel_fname);
499
+ this.outbound.set(tag.rel_fname, 0);
500
+ } else {
501
+ let counts = references.get(tag.name);
502
+ if (!counts) references.set(tag.name, counts = /* @__PURE__ */ new Map());
503
+ counts.set(tag.rel_fname, (counts.get(tag.rel_fname) ?? 0) + (tag.count ?? 1));
501
504
  }
502
505
  }
503
- const idents = new Set(Array.from(defines.keys()).filter((k) => references.has(k)));
504
- for (const ident of Array.from(idents)) {
505
- const definers = Array.from(defines.get(ident) || []);
506
- const referencers = references.get(ident) || [];
507
- let mul = 1;
508
- if (this.isSnakeCase(ident) || this.isKebabCase(ident) || this.isCamelCase(ident)) {
509
- mul *= 10;
510
- }
511
- if (ident.startsWith("_")) {
512
- mul *= 0.1;
513
- }
514
- if (definers.length > 5) {
515
- mul *= 0.1;
516
- }
517
- const refCounts = /* @__PURE__ */ new Map();
518
- for (const ref of referencers) {
519
- refCounts.set(ref, (refCounts.get(ref) || 0) + 1);
520
- }
521
- for (const referencer of referencers) {
522
- const numRefs = refCounts.get(referencer) || 1;
523
- const weight = mul * Math.sqrt(numRefs);
524
- for (const definer of definers) {
525
- if (referencer !== definer) {
526
- if (!graph.hasNode(referencer)) {
527
- graph.addNode(referencer);
528
- }
529
- if (!graph.hasNode(definer)) {
530
- graph.addNode(definer);
531
- }
532
- const edgeKey = `${referencer}->${definer}:${ident}`;
533
- if (!graph.hasEdge(edgeKey)) {
534
- graph.addEdgeWithKey(edgeKey, referencer, definer, { weight, ident });
535
- }
536
- }
537
- }
506
+ for (const [name, definers] of definitions) {
507
+ const counts = references.get(name);
508
+ if (!counts) continue;
509
+ let multiplier = 1;
510
+ if (/^[a-z][a-z0-9_]*$/.test(name) && name.includes("_") || /^[a-z][a-z0-9-]*$/.test(name) && name.includes("-") || /^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name))
511
+ multiplier *= 10;
512
+ if (name.startsWith("_")) multiplier *= 0.1;
513
+ if (definers.size > 5) multiplier *= 0.1;
514
+ const sources = /* @__PURE__ */ new Map();
515
+ for (const [file, count] of counts) {
516
+ const targets = definers.size - Number(definers.has(file));
517
+ if (!targets) continue;
518
+ const weight = multiplier * Math.sqrt(count);
519
+ sources.set(file, weight);
520
+ this.outbound.set(file, (this.outbound.get(file) ?? 0) + weight * targets);
538
521
  }
522
+ if (sources.size) this.relations.push({ definers, sources });
539
523
  }
540
- logger_default.info(`[GraphBuilder] Built graph with ${graph.order} nodes and ${graph.size} edges`);
541
- return { graph, defines, references };
542
- }
543
- isSnakeCase(str) {
544
- return /^[a-z][a-z0-9_]*$/.test(str) && str.includes("_");
545
524
  }
546
- isKebabCase(str) {
547
- return /^[a-z][a-z0-9-]*$/.test(str) && str.includes("-");
548
- }
549
- isCamelCase(str) {
550
- return /^[a-z][a-zA-Z0-9]*$/.test(str) && /[A-Z]/.test(str);
551
- }
552
- };
553
-
554
- // src/pagerank.ts
555
- var import_pagerank = __toESM(require("graphology-metrics/centrality/pagerank"));
556
- var PageRankCalculator = class {
557
- calculate(graph, tags, mentionedFiles, mentionedIdents) {
558
- if (graph.order === 0) {
559
- return [];
560
- }
561
- const personalization = {};
562
- const nodes = graph.nodes();
563
- const personalize = nodes.length > 0 ? 100 / nodes.length : 0;
564
- for (const node of nodes) {
565
- let score = 0;
566
- if (mentionedFiles?.has(node)) {
567
- score += personalize;
525
+ rank(mentionedFiles = /* @__PURE__ */ new Set(), mentionedIdents = /* @__PURE__ */ new Set()) {
526
+ const nodes = [...this.outbound.keys()];
527
+ const count = nodes.length;
528
+ if (!count) return [];
529
+ const mentionedDefinitions = new Set(this.definitions.filter((tag) => mentionedIdents.has(tag.name)).map((tag) => tag.rel_fname));
530
+ const weights = new Map(nodes.map((node) => [node, Number(mentionedFiles.has(node)) + Number(mentionedDefinitions.has(node))]));
531
+ const total = [...weights.values()].reduce((sum, weight) => sum + weight, 0);
532
+ const personalization = new Map(nodes.map((node) => [node, total ? weights.get(node) / total : 1 / count]));
533
+ let ranks = new Map(nodes.map((node) => [node, 1 / count]));
534
+ for (let iteration = 0; iteration < 100; iteration++) {
535
+ let dangling = 0;
536
+ for (const node of nodes) {
537
+ if (!this.outbound.get(node)) dangling += ranks.get(node);
568
538
  }
569
- if (mentionedIdents) {
570
- const fileTags = tags.filter((t) => t.rel_fname === node && t.kind === "def");
571
- if (fileTags.some((t) => mentionedIdents.has(t.name))) {
572
- score += personalize;
539
+ const base = 0.85 * dangling + 0.15;
540
+ const next = new Map(nodes.map((node) => [node, base * personalization.get(node)]));
541
+ for (const { definers, sources } of this.relations) {
542
+ let incoming = 0;
543
+ for (const [source, weight] of sources) {
544
+ incoming += 0.85 * ranks.get(source) * weight / this.outbound.get(source);
573
545
  }
574
- }
575
- if (score > 0) {
576
- personalization[node] = score;
577
- }
578
- }
579
- try {
580
- const pagerankOptions = {
581
- alpha: 0.85,
582
- maxIterations: 100,
583
- tolerance: 1e-6
584
- };
585
- if (Object.keys(personalization).length > 0) {
586
- pagerankOptions.personalization = personalization;
587
- }
588
- const ranks = (0, import_pagerank.default)(graph, pagerankOptions);
589
- const rankedDefinitions = [];
590
- const definitions = tags.filter((t) => t.kind === "def");
591
- for (const def of definitions) {
592
- const nodeRank = ranks[def.rel_fname] || 0;
593
- let edgeRank = nodeRank;
594
- if (graph.hasNode(def.rel_fname)) {
595
- graph.forEachOutboundEdge(def.rel_fname, (edge) => {
596
- const weight = graph.getEdgeAttribute(edge, "weight");
597
- const totalWeight = this.getTotalOutboundWeight(graph, def.rel_fname);
598
- if (totalWeight > 0) {
599
- edgeRank += nodeRank * (weight / totalWeight);
600
- }
601
- });
546
+ for (const target of definers) {
547
+ const self = sources.get(target);
548
+ const contribution = incoming - (self ? 0.85 * ranks.get(target) * self / this.outbound.get(target) : 0);
549
+ next.set(target, next.get(target) + contribution);
602
550
  }
603
- rankedDefinitions.push({
604
- rel_fname: def.rel_fname,
605
- name: def.name,
606
- rank: edgeRank,
607
- line: def.line
608
- });
609
551
  }
610
- rankedDefinitions.sort((a, b) => b.rank - a.rank);
611
- logger_default.info(`[PageRankCalculator] Calculated ranks for ${rankedDefinitions.length} definitions`);
612
- return rankedDefinitions;
613
- } catch (error) {
614
- logger_default.error("[PageRankCalculator] Error calculating PageRank:", error);
615
- return [];
552
+ let error = 0;
553
+ for (const node of nodes) error += Math.abs(next.get(node) - ranks.get(node));
554
+ ranks = next;
555
+ if (error < count * 1e-6) break;
556
+ if (iteration === 99) throw new Error("Repository map PageRank did not converge");
616
557
  }
617
- }
618
- getTotalOutboundWeight(graph, node) {
619
- if (!graph.hasNode(node)) {
620
- return 0;
621
- }
622
- let total = 0;
623
- graph.forEachOutboundEdge(node, (edge) => {
624
- total += graph.getEdgeAttribute(edge, "weight") || 1;
625
- });
626
- return total;
558
+ return this.definitions.map((tag) => ({
559
+ rel_fname: tag.rel_fname,
560
+ name: tag.name,
561
+ line: tag.line,
562
+ rank: ranks.get(tag.rel_fname) * (this.outbound.get(tag.rel_fname) ? 2 : 1)
563
+ })).sort((a, b) => b.rank - a.rank);
627
564
  }
628
565
  };
629
566
 
630
567
  // src/tree-renderer.ts
631
- var fs3 = __toESM(require("fs/promises"));
568
+ var fs4 = __toESM(require("fs/promises"));
569
+ var path6 = __toESM(require("path"));
632
570
  var TreeRenderer = class {
633
- async render(rankedDefinitions, chatFiles, maxLines) {
571
+ async render(rankedDefinitions, chatFiles, maxLines, root = process.cwd()) {
634
572
  const filtered = rankedDefinitions.filter((def) => !chatFiles.has(def.rel_fname));
635
573
  const byFile = /* @__PURE__ */ new Map();
636
574
  for (const def of filtered) {
@@ -638,24 +576,26 @@ var TreeRenderer = class {
638
576
  fileDefs.push(def);
639
577
  byFile.set(def.rel_fname, fileDefs);
640
578
  }
579
+ const maximumRanks = /* @__PURE__ */ new Map();
580
+ for (const def of filtered) maximumRanks.set(def.rel_fname, Math.max(maximumRanks.get(def.rel_fname) ?? -Infinity, def.rank));
641
581
  const sortedFiles = Array.from(byFile.entries()).sort((a, b) => {
642
- const maxRankA = Math.max(...a[1].map((d) => d.rank));
643
- const maxRankB = Math.max(...b[1].map((d) => d.rank));
582
+ const maxRankA = maximumRanks.get(a[0]);
583
+ const maxRankB = maximumRanks.get(b[0]);
644
584
  return maxRankB - maxRankA;
645
585
  });
646
586
  const totalFiles = sortedFiles.length;
647
587
  if (!maxLines) {
648
- return this.renderAllFiles(sortedFiles);
588
+ return this.renderAllFiles(sortedFiles, root);
649
589
  }
650
- if (totalFiles > maxLines) {
590
+ if (totalFiles * 2 > maxLines) {
651
591
  return this.renderFileListOnly(sortedFiles, maxLines);
652
592
  }
653
593
  const headerLines = totalFiles;
654
594
  const availableLines = maxLines - headerLines;
655
595
  const linesPerFile = Math.max(1, Math.floor(availableLines / totalFiles));
656
- return this.renderWithLineLimit(sortedFiles, linesPerFile);
596
+ return this.renderWithLineLimit(sortedFiles, linesPerFile, root);
657
597
  }
658
- async renderAllFiles(sortedFiles) {
598
+ async renderAllFiles(sortedFiles, root) {
659
599
  let output = "";
660
600
  for (const [file, definitions] of sortedFiles) {
661
601
  output += `
@@ -663,7 +603,7 @@ ${file}:
663
603
  `;
664
604
  definitions.sort((a, b) => a.line - b.line);
665
605
  const lines = Array.from(new Set(definitions.map((d) => d.line)));
666
- const treeLines = await this.renderFileTree(file, lines, Infinity);
606
+ const treeLines = await this.renderFileTree(path6.resolve(root, file), lines, Infinity);
667
607
  output += treeLines;
668
608
  }
669
609
  return output.trim();
@@ -680,7 +620,7 @@ ${file}:
680
620
  }
681
621
  return output.trim();
682
622
  }
683
- async renderWithLineLimit(sortedFiles, linesPerFile) {
623
+ async renderWithLineLimit(sortedFiles, linesPerFile, root) {
684
624
  let output = "";
685
625
  for (const [file, definitions] of sortedFiles) {
686
626
  definitions.sort((a, b) => {
@@ -694,7 +634,7 @@ ${file}:
694
634
  const isTruncated = uniqueLines.length > linesPerFile;
695
635
  output += `${file}:
696
636
  `;
697
- const treeLines = await this.renderFileTree(file, limitedLines, linesPerFile, isTruncated);
637
+ const treeLines = await this.renderFileTree(path6.resolve(root, file), limitedLines, linesPerFile, isTruncated);
698
638
  output += treeLines;
699
639
  }
700
640
  return output.trim();
@@ -704,7 +644,7 @@ ${file}:
704
644
  return "";
705
645
  }
706
646
  try {
707
- const content = await fs3.readFile(filePath, "utf-8");
647
+ const content = await fs4.readFile(filePath, "utf-8");
708
648
  const fileLines = content.split("\n");
709
649
  let output = "";
710
650
  const maxContentLines = isTruncated ? maxLines - 1 : maxLines;
@@ -733,119 +673,102 @@ ${file}:
733
673
 
734
674
  // src/cache-manager.ts
735
675
  var import_node_sqlite = require("node:sqlite");
736
- var fs4 = __toESM(require("fs/promises"));
737
- var path5 = __toESM(require("path"));
676
+ var fs5 = __toESM(require("fs/promises"));
677
+ var path7 = __toESM(require("path"));
738
678
  var os2 = __toESM(require("os"));
739
- function getCacheDbPath() {
740
- const platform3 = os2.platform();
741
- const homeDir = os2.homedir();
742
- let cacheDir;
743
- if (platform3 === "darwin") {
744
- cacheDir = path5.join(homeDir, "Library", "Caches", "aiderdesk", "tree-sitter");
745
- } else if (platform3 === "win32") {
746
- cacheDir = path5.join(process.env.APPDATA || path5.join(homeDir, "AppData", "Roaming"), "aiderdesk", "tree-sitter");
747
- } else {
748
- cacheDir = path5.join(homeDir, ".cache", "aiderdesk", "tree-sitter");
749
- }
750
- return path5.join(cacheDir, "cache.db");
751
- }
679
+ var getCacheDbPath = () => {
680
+ const home = os2.homedir();
681
+ const directory = process.platform === "darwin" ? path7.join(home, "Library", "Caches") : process.platform === "win32" ? process.env.APPDATA || path7.join(home, "AppData", "Roaming") : path7.join(home, ".cache");
682
+ return path7.join(directory, "aiderdesk", "tree-sitter", "cache.db");
683
+ };
752
684
  var CacheManager = class {
753
- db = null;
754
- dbPath;
755
- constructor(dbPath) {
756
- this.dbPath = dbPath || getCacheDbPath();
685
+ constructor(dbPath = getCacheDbPath()) {
686
+ this.dbPath = dbPath;
757
687
  }
688
+ db = null;
689
+ select;
690
+ insert;
758
691
  async initialize() {
759
692
  try {
760
- await fs4.mkdir(path5.dirname(this.dbPath), { recursive: true });
693
+ await fs5.mkdir(path7.dirname(this.dbPath), { recursive: true });
761
694
  this.db = new import_node_sqlite.DatabaseSync(this.dbPath);
762
- const createTableSQL = `
763
- CREATE TABLE IF NOT EXISTS file_cache (
695
+ this.db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 1000;");
696
+ this.db.exec(`
697
+ CREATE TABLE IF NOT EXISTS file_cache_v3 (
764
698
  file_path TEXT PRIMARY KEY,
765
- mtime INTEGER NOT NULL,
766
- tags TEXT NOT NULL,
767
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
768
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
699
+ mtime REAL NOT NULL,
700
+ fingerprint TEXT NOT NULL,
701
+ tags TEXT NOT NULL
769
702
  )
770
- `;
771
- this.db.exec(createTableSQL);
772
- logger_default.info(`[CacheManager] Cache database initialized at ${this.dbPath}`);
703
+ `);
704
+ this.select = this.db.prepare("SELECT mtime, fingerprint, tags FROM file_cache_v3 WHERE file_path = ?");
705
+ this.insert = this.db.prepare(`
706
+ INSERT INTO file_cache_v3 (file_path, mtime, fingerprint, tags) VALUES (?, ?, ?, ?)
707
+ ON CONFLICT(file_path) DO UPDATE SET mtime = excluded.mtime, fingerprint = excluded.fingerprint, tags = excluded.tags
708
+ `);
773
709
  } catch (error) {
774
- logger_default.error("[CacheManager] Failed to initialize cache database:", error);
775
- this.db = null;
710
+ logger_default.warn("[CacheManager] Persistent cache unavailable:", error);
711
+ this.close();
776
712
  }
777
713
  }
778
- async getFileCache(filePath) {
779
- if (!this.db) {
780
- return null;
781
- }
714
+ async getFileCache(file) {
782
715
  try {
783
- const sql = "SELECT mtime, tags FROM file_cache WHERE file_path = ?";
784
- const stmt = this.db.prepare(sql);
785
- const result = stmt.get(filePath);
786
- if (result) {
787
- const tags = JSON.parse(result.tags);
788
- return { tags, mtime: result.mtime };
789
- }
790
- return null;
716
+ const result = this.select?.get(file);
717
+ if (!result) return null;
718
+ const stored = JSON.parse(result.tags);
719
+ return {
720
+ ...result,
721
+ tags: stored.tags.map(([line, name, kind, count]) => ({
722
+ fname: file,
723
+ rel_fname: stored.relative,
724
+ line,
725
+ name,
726
+ kind,
727
+ count
728
+ }))
729
+ };
791
730
  } catch (error) {
792
- logger_default.error(`[CacheManager] Failed to get cache for ${filePath}:`, error);
731
+ logger_default.warn(`[CacheManager] Failed to read ${file}:`, error);
793
732
  return null;
794
733
  }
795
734
  }
796
- async setFileCache(filePath, mtime, tags) {
797
- if (!this.db) {
798
- return;
799
- }
735
+ async setFileCache(file, mtime, tags, fingerprint = String(mtime)) {
800
736
  try {
801
- const sql = `
802
- INSERT INTO file_cache (file_path, mtime, tags, updated_at)
803
- VALUES (?, ?, ?, CURRENT_TIMESTAMP)
804
- ON CONFLICT(file_path) DO UPDATE SET
805
- mtime = excluded.mtime,
806
- tags = excluded.tags,
807
- updated_at = CURRENT_TIMESTAMP
808
- `;
809
- const stmt = this.db.prepare(sql);
810
- stmt.run(filePath, mtime, JSON.stringify(tags));
811
- logger_default.debug(`[CacheManager] Cached ${tags.length} tags for ${filePath}`);
737
+ this.insert?.run(
738
+ file,
739
+ mtime,
740
+ fingerprint,
741
+ JSON.stringify({
742
+ relative: tags[0]?.rel_fname ?? file,
743
+ tags: tags.map((tag) => [tag.line, tag.name, tag.kind, tag.count])
744
+ })
745
+ );
812
746
  } catch (error) {
813
- logger_default.error(`[CacheManager] Failed to set cache for ${filePath}:`, error);
747
+ logger_default.warn(`[CacheManager] Failed to cache ${file}:`, error);
814
748
  }
815
749
  }
816
- async getFileMtime(filePath) {
750
+ async getFileMtime(file) {
817
751
  try {
818
- const stats = await fs4.stat(filePath);
819
- return stats.mtimeMs;
820
- } catch (error) {
821
- logger_default.error(`[CacheManager] Failed to get mtime for ${filePath}:`, error);
752
+ return (await fs5.stat(file)).mtimeMs;
753
+ } catch {
822
754
  return null;
823
755
  }
824
756
  }
825
757
  async clearCache() {
826
- if (!this.db) {
827
- return;
828
- }
829
- try {
830
- const sql = "DELETE FROM file_cache";
831
- this.db.exec(sql);
832
- logger_default.info("[CacheManager] Cache cleared");
833
- } catch (error) {
834
- logger_default.error("[CacheManager] Failed to clear cache:", error);
835
- }
758
+ this.db?.exec("DELETE FROM file_cache_v3");
836
759
  }
837
760
  close() {
838
- if (this.db) {
839
- this.db.close();
840
- this.db = null;
841
- logger_default.info("[CacheManager] Cache database closed");
842
- }
761
+ this.select = void 0;
762
+ this.insert = void 0;
763
+ this.db?.close();
764
+ this.db = null;
843
765
  }
844
766
  };
845
767
 
846
768
  // src/file-utils.ts
847
- var path6 = __toESM(require("path"));
769
+ var path8 = __toESM(require("path"));
848
770
  var import_glob = require("glob");
771
+ var import_minimatch = require("minimatch");
849
772
  var import_simple_git = require("simple-git");
850
773
  var DEFAULT_IGNORE_PATTERNS = [
851
774
  "node_modules/**",
@@ -865,170 +788,430 @@ var DEFAULT_IGNORE_PATTERNS = [
865
788
  "yarn.lock",
866
789
  "pnpm-lock.yaml"
867
790
  ];
868
- var isGitRepository = async (baseDir) => {
869
- try {
870
- const git = (0, import_simple_git.simpleGit)(baseDir);
871
- await git.revparse(["--git-dir"]);
872
- return true;
873
- } catch {
874
- return false;
875
- }
876
- };
877
791
  var getAllFilesFromGit = async (baseDir) => {
878
792
  try {
879
- const git = (0, import_simple_git.simpleGit)(baseDir);
880
- const result = await git.raw(["ls-files", "-z"]);
881
- const files = result.split("\0").filter(Boolean);
882
- logger_default.debug(`Retrieved ${files.length} tracked files from Git`);
883
- return files.map((file) => path6.resolve(baseDir, file));
884
- } catch (gitError) {
885
- logger_default.warn("Failed to get tracked files from Git, falling back to filesystem", gitError);
886
- return [];
887
- }
888
- };
889
- var getAllFilesFromFS = async (baseDir, ignore) => {
890
- try {
891
- const pattern = path6.join(baseDir, "**/*").replace(/\\/g, "/");
892
- const files = await (0, import_glob.glob)(pattern, {
893
- nodir: true,
894
- absolute: true,
895
- dot: false,
896
- ignore
897
- });
898
- logger_default.debug(`Retrieved ${files.length} files from filesystem`);
899
- return files;
793
+ const result = await (0, import_simple_git.simpleGit)(baseDir).raw(["ls-files", "-z"]);
794
+ return result.split(String.fromCharCode(0)).filter(Boolean).map((file) => path8.resolve(baseDir, file));
900
795
  } catch (error) {
901
- logger_default.error("Failed to get files from filesystem:", error);
902
- return [];
796
+ logger_default.debug("Git discovery unavailable, falling back to filesystem", error);
797
+ return null;
903
798
  }
904
799
  };
905
- var matchesPattern = (filePath, pattern) => {
906
- const regexPattern = pattern.replace(/\*\*/g, "<<DOUBLE_STAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLE_STAR>>/g, ".*").replace(/\?/g, "[^/]");
907
- const regex = new RegExp(regexPattern);
908
- return regex.test(filePath);
909
- };
910
- var filterByExcludePatterns = (files, excludePatterns, baseDir) => {
911
- return files.filter((file) => {
912
- const relative3 = path6.relative(baseDir, file).replace(/\\/g, "/");
913
- return !excludePatterns.some((pattern) => matchesPattern(relative3, pattern));
914
- });
915
- };
916
800
  var getAllFiles = async (baseDir, excludePatterns = []) => {
917
- const allIgnorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...excludePatterns];
801
+ const ignore = [...DEFAULT_IGNORE_PATTERNS, ...excludePatterns];
802
+ const matchers = ignore.map((pattern) => ({
803
+ regex: new import_minimatch.Minimatch(pattern, { dot: true }).makeRe(),
804
+ basename: !pattern.includes("/")
805
+ }));
806
+ const gitFiles = await getAllFilesFromGit(baseDir);
807
+ if (gitFiles !== null) {
808
+ return gitFiles.filter((file) => {
809
+ const relative5 = path8.relative(baseDir, file).split(path8.sep).join("/");
810
+ const basename2 = path8.basename(file);
811
+ return !matchers.some((matcher) => matcher.regex && matcher.regex.test(matcher.basename ? basename2 : relative5));
812
+ });
813
+ }
918
814
  try {
919
- if (await isGitRepository(baseDir)) {
920
- const gitFiles = await getAllFilesFromGit(baseDir);
921
- if (gitFiles.length > 0) {
922
- return filterByExcludePatterns(gitFiles, excludePatterns, baseDir);
923
- }
924
- }
925
- const fsFiles = await getAllFilesFromFS(baseDir, allIgnorePatterns);
926
- return filterByExcludePatterns(fsFiles, excludePatterns, baseDir);
815
+ return await (0, import_glob.glob)("**/*", { cwd: baseDir, nodir: true, absolute: true, dot: false, ignore });
927
816
  } catch (error) {
928
- logger_default.error("Failed to get files:", error);
817
+ logger_default.error("Failed to discover repository files:", error);
929
818
  return [];
930
819
  }
931
820
  };
932
821
 
933
822
  // src/tree-sitter-analyzer.ts
934
823
  var TreeSitterAnalyzer = class {
935
- parser;
936
- symbolExtractor;
937
- graphBuilder;
938
- pageRankCalculator;
939
- treeRenderer;
824
+ parser = new TreeSitterParser();
825
+ symbolExtractor = new SymbolExtractor(this.parser);
826
+ treeRenderer = new TreeRenderer();
940
827
  cacheManager;
941
- initialized = false;
942
- constructor() {
943
- this.parser = new TreeSitterParser();
944
- this.cacheManager = new CacheManager();
945
- this.symbolExtractor = new SymbolExtractor(this.parser);
828
+ initialization;
829
+ queue = Promise.resolve();
830
+ graph;
831
+ revision = -1;
832
+ outputs = /* @__PURE__ */ new Map();
833
+ constructor(options = {}) {
834
+ this.cacheManager = new CacheManager(options.cachePath);
946
835
  this.symbolExtractor.setCacheManager(this.cacheManager);
947
- this.graphBuilder = new GraphBuilder();
948
- this.pageRankCalculator = new PageRankCalculator();
949
- this.treeRenderer = new TreeRenderer();
950
836
  }
951
- async initialize() {
952
- if (this.initialized) {
953
- return;
837
+ initialize() {
838
+ this.initialization ??= Promise.all([this.parser.initialize(), this.cacheManager.initialize()]).then(() => void 0);
839
+ return this.initialization;
840
+ }
841
+ getRepoMap(options) {
842
+ const result = this.queue.then(() => this.generateRepoMap(options));
843
+ this.queue = result.catch(() => void 0);
844
+ return result;
845
+ }
846
+ async generateRepoMap(options) {
847
+ options.signal?.throwIfAborted();
848
+ await this.initialize();
849
+ const root = path9.resolve(options.root);
850
+ const normalize = (files2 = /* @__PURE__ */ new Set()) => new Set([...files2].map((file) => path9.relative(root, path9.resolve(root, file)).replaceAll(path9.sep, "/")));
851
+ const mentionedFiles = normalize(options.mentionedFiles);
852
+ const chatFiles = normalize(options.chatFiles);
853
+ const mentionedIdents = options.mentionedIdents ?? /* @__PURE__ */ new Set();
854
+ const files = options.files?.length ? options.files.map((file) => path9.resolve(root, file)) : await getAllFiles(root, options.excludePatterns);
855
+ options.signal?.throwIfAborted();
856
+ const tags = await this.symbolExtractor.extractTags(files, root, options.signal, true);
857
+ if (!tags.length) return "";
858
+ if (this.revision !== this.symbolExtractor.revision) {
859
+ this.graph = new DependencyIndex(tags);
860
+ this.revision = this.symbolExtractor.revision;
861
+ this.outputs.clear();
954
862
  }
955
- await this.parser.initialize();
956
- await this.cacheManager.initialize();
957
- this.initialized = true;
958
- logger_default.info("[TreeSitterAnalyzer] Initialized");
863
+ if (options.prepareOnly) return "";
864
+ const key = JSON.stringify([[...mentionedFiles].sort(), [...mentionedIdents].sort(), [...chatFiles].sort(), options.maxLines]);
865
+ const cached = this.outputs.get(key);
866
+ if (cached !== void 0) return cached;
867
+ options.signal?.throwIfAborted();
868
+ const ranked = this.graph.rank(mentionedFiles, mentionedIdents);
869
+ options.signal?.throwIfAborted();
870
+ const output = await this.treeRenderer.render(ranked, chatFiles, options.maxLines, root);
871
+ options.signal?.throwIfAborted();
872
+ if (this.outputs.size >= 8) this.outputs.delete(this.outputs.keys().next().value);
873
+ this.outputs.set(key, output);
874
+ return output;
959
875
  }
960
- async getRepoMap(options) {
961
- if (!this.initialized) {
876
+ extractSymbols(filePaths, options) {
877
+ const result = this.queue.then(async () => {
962
878
  await this.initialize();
879
+ if (options?.useCache === false) this.symbolExtractor.setCacheManager(null);
880
+ try {
881
+ const tags = await this.symbolExtractor.extractTags(filePaths, options?.rootDir);
882
+ return tags.map((tag) => ({
883
+ name: tag.name,
884
+ kind: tag.kind,
885
+ line: tag.line,
886
+ file: tag.fname,
887
+ relativeFile: tag.rel_fname
888
+ }));
889
+ } finally {
890
+ this.symbolExtractor.setCacheManager(this.cacheManager);
891
+ }
892
+ });
893
+ this.queue = result.catch(() => void 0);
894
+ return result;
895
+ }
896
+ async parseFile(filePath) {
897
+ await this.initialize();
898
+ const { detectLanguage: detectLanguage2 } = await Promise.resolve().then(() => (init_language_detector(), language_detector_exports));
899
+ return {
900
+ tags: await this.parser.parseFile(filePath),
901
+ file: filePath,
902
+ language: detectLanguage2(filePath) || "unknown"
903
+ };
904
+ }
905
+ async dispose() {
906
+ await this.queue;
907
+ await this.parser.dispose();
908
+ this.cacheManager.close();
909
+ this.graph = void 0;
910
+ this.outputs.clear();
911
+ }
912
+ };
913
+
914
+ // src/graph-builder.ts
915
+ var import_graphology = __toESM(require("graphology"));
916
+ var GraphBuilder = class {
917
+ buildDependencyGraph(tags) {
918
+ const graph = new import_graphology.default({ type: "directed", allowSelfLoops: false, multi: true });
919
+ const defines = /* @__PURE__ */ new Map();
920
+ const references = /* @__PURE__ */ new Map();
921
+ const definitions = /* @__PURE__ */ new Map();
922
+ for (const tag of tags) {
923
+ if (tag.kind === "def") {
924
+ if (!defines.has(tag.name)) {
925
+ defines.set(tag.name, /* @__PURE__ */ new Set());
926
+ }
927
+ defines.get(tag.name).add(tag.rel_fname);
928
+ const key = `${tag.rel_fname}:${tag.name}`;
929
+ if (!definitions.has(key)) {
930
+ definitions.set(key, []);
931
+ }
932
+ definitions.get(key).push(tag);
933
+ } else if (tag.kind === "ref") {
934
+ if (!references.has(tag.name)) {
935
+ references.set(tag.name, []);
936
+ }
937
+ references.get(tag.name).push(tag.rel_fname);
938
+ }
963
939
  }
964
- const { root, files, excludePatterns = [], mentionedFiles = /* @__PURE__ */ new Set(), mentionedIdents = /* @__PURE__ */ new Set(), chatFiles = /* @__PURE__ */ new Set(), maxLines } = options;
965
- try {
966
- let filesToProcess;
967
- if (files && files.length > 0) {
968
- filesToProcess = files;
969
- } else {
970
- filesToProcess = await getAllFiles(root, excludePatterns);
940
+ const idents = new Set(Array.from(defines.keys()).filter((k) => references.has(k)));
941
+ for (const ident of Array.from(idents)) {
942
+ const definers = Array.from(defines.get(ident) || []);
943
+ const referencers = references.get(ident) || [];
944
+ let mul = 1;
945
+ if (this.isSnakeCase(ident) || this.isKebabCase(ident) || this.isCamelCase(ident)) {
946
+ mul *= 10;
947
+ }
948
+ if (ident.startsWith("_")) {
949
+ mul *= 0.1;
971
950
  }
972
- logger_default.info(`[TreeSitterAnalyzer] Processing ${filesToProcess.length} files for repository map`);
973
- const tags = await this.symbolExtractor.extractTags(filesToProcess, root);
974
- if (tags.length === 0) {
975
- return "";
951
+ if (definers.length > 5) {
952
+ mul *= 0.1;
953
+ }
954
+ const refCounts = /* @__PURE__ */ new Map();
955
+ for (const ref of referencers) {
956
+ refCounts.set(ref, (refCounts.get(ref) || 0) + 1);
957
+ }
958
+ for (const referencer of referencers) {
959
+ const numRefs = refCounts.get(referencer) || 1;
960
+ const weight = mul * Math.sqrt(numRefs);
961
+ for (const definer of definers) {
962
+ if (referencer !== definer) {
963
+ if (!graph.hasNode(referencer)) {
964
+ graph.addNode(referencer);
965
+ }
966
+ if (!graph.hasNode(definer)) {
967
+ graph.addNode(definer);
968
+ }
969
+ const edgeKey = `${referencer}->${definer}:${ident}`;
970
+ if (!graph.hasEdge(edgeKey)) {
971
+ graph.addEdgeWithKey(edgeKey, referencer, definer, { weight, ident });
972
+ }
973
+ }
974
+ }
976
975
  }
977
- const { graph, defines: _defines, references: _references } = this.graphBuilder.buildDependencyGraph(tags);
978
- const rankedDefinitions = this.pageRankCalculator.calculate(graph, tags, mentionedFiles, mentionedIdents);
979
- const output = await this.treeRenderer.render(rankedDefinitions, chatFiles, maxLines);
980
- logger_default.info(`[TreeSitterAnalyzer] Generated repository map with ${rankedDefinitions.length} definitions`);
981
- return output;
982
- } catch (error) {
983
- logger_default.error("[TreeSitterAnalyzer] Error generating repository map:", error);
984
- return "";
985
976
  }
977
+ logger_default.info(`[GraphBuilder] Built graph with ${graph.order} nodes and ${graph.size} edges`);
978
+ return { graph, defines, references };
986
979
  }
987
- async extractSymbols(filePaths, options) {
988
- if (!this.initialized) {
989
- await this.initialize();
980
+ isSnakeCase(str) {
981
+ return /^[a-z][a-z0-9_]*$/.test(str) && str.includes("_");
982
+ }
983
+ isKebabCase(str) {
984
+ return /^[a-z][a-z0-9-]*$/.test(str) && str.includes("-");
985
+ }
986
+ isCamelCase(str) {
987
+ return /^[a-z][a-zA-Z0-9]*$/.test(str) && /[A-Z]/.test(str);
988
+ }
989
+ };
990
+
991
+ // src/pagerank.ts
992
+ var import_pagerank = __toESM(require("graphology-metrics/centrality/pagerank"));
993
+ var PageRankCalculator = class {
994
+ calculate(graph, tags, mentionedFiles, mentionedIdents) {
995
+ if (graph.order === 0) {
996
+ return [];
997
+ }
998
+ const personalization = {};
999
+ const nodes = graph.nodes();
1000
+ const personalize = nodes.length > 0 ? 100 / nodes.length : 0;
1001
+ for (const node of nodes) {
1002
+ let score = 0;
1003
+ if (mentionedFiles?.has(node)) {
1004
+ score += personalize;
1005
+ }
1006
+ if (mentionedIdents) {
1007
+ const fileTags = tags.filter((t) => t.rel_fname === node && t.kind === "def");
1008
+ if (fileTags.some((t) => mentionedIdents.has(t.name))) {
1009
+ score += personalize;
1010
+ }
1011
+ }
1012
+ if (score > 0) {
1013
+ personalization[node] = score;
1014
+ }
990
1015
  }
991
- const useCache = options?.useCache !== false;
992
1016
  try {
993
- const originalCacheManager = useCache ? this.cacheManager : null;
994
- if (!useCache) {
995
- this.symbolExtractor.setCacheManager(null);
1017
+ const pagerankOptions = {
1018
+ alpha: 0.85,
1019
+ maxIterations: 100,
1020
+ tolerance: 1e-6
1021
+ };
1022
+ if (Object.keys(personalization).length > 0) {
1023
+ pagerankOptions.personalization = personalization;
996
1024
  }
997
- const tags = await this.symbolExtractor.extractTags(filePaths, options?.rootDir);
998
- if (!useCache) {
999
- this.symbolExtractor.setCacheManager(this.cacheManager);
1025
+ const ranks = (0, import_pagerank.default)(graph, pagerankOptions);
1026
+ const rankedDefinitions = [];
1027
+ const definitions = tags.filter((t) => t.kind === "def");
1028
+ for (const def of definitions) {
1029
+ const nodeRank = ranks[def.rel_fname] || 0;
1030
+ let edgeRank = nodeRank;
1031
+ if (graph.hasNode(def.rel_fname)) {
1032
+ graph.forEachOutboundEdge(def.rel_fname, (edge) => {
1033
+ const weight = graph.getEdgeAttribute(edge, "weight");
1034
+ const totalWeight = this.getTotalOutboundWeight(graph, def.rel_fname);
1035
+ if (totalWeight > 0) {
1036
+ edgeRank += nodeRank * (weight / totalWeight);
1037
+ }
1038
+ });
1039
+ }
1040
+ rankedDefinitions.push({
1041
+ rel_fname: def.rel_fname,
1042
+ name: def.name,
1043
+ rank: edgeRank,
1044
+ line: def.line
1045
+ });
1000
1046
  }
1001
- const symbols = tags.map((tag) => ({
1002
- name: tag.name,
1003
- kind: tag.kind,
1004
- line: tag.line,
1005
- file: tag.fname,
1006
- relativeFile: tag.rel_fname
1007
- }));
1008
- logger_default.info(`[TreeSitterAnalyzer] Extracted ${symbols.length} symbols from ${filePaths.length} files`);
1009
- return symbols;
1047
+ rankedDefinitions.sort((a, b) => b.rank - a.rank);
1048
+ logger_default.info(`[PageRankCalculator] Calculated ranks for ${rankedDefinitions.length} definitions`);
1049
+ return rankedDefinitions;
1010
1050
  } catch (error) {
1011
- logger_default.error("[TreeSitterAnalyzer] Error extracting symbols:", error);
1051
+ logger_default.error("[PageRankCalculator] Error calculating PageRank:", error);
1012
1052
  return [];
1013
1053
  }
1014
1054
  }
1015
- async parseFile(filePath) {
1016
- if (!this.initialized) {
1017
- await this.initialize();
1055
+ getTotalOutboundWeight(graph, node) {
1056
+ if (!graph.hasNode(node)) {
1057
+ return 0;
1018
1058
  }
1019
- const { detectLanguage: detectLanguage2 } = await Promise.resolve().then(() => (init_language_detector(), language_detector_exports));
1020
- const language = detectLanguage2(filePath) || "unknown";
1021
- const tags = await this.parser.parseFile(filePath);
1022
- return {
1023
- tags,
1024
- file: filePath,
1025
- language
1026
- };
1059
+ let total = 0;
1060
+ graph.forEachOutboundEdge(node, (edge) => {
1061
+ total += graph.getEdgeAttribute(edge, "weight") || 1;
1062
+ });
1063
+ return total;
1027
1064
  }
1028
1065
  };
1029
1066
 
1030
1067
  // src/index.ts
1031
1068
  init_language_detector();
1069
+
1070
+ // src/repo-map-service.ts
1071
+ var path10 = __toESM(require("path"));
1072
+ var import_worker_threads = require("worker_threads");
1073
+ var RepoMapService = class {
1074
+ constructor(options = {}) {
1075
+ this.options = options;
1076
+ }
1077
+ worker;
1078
+ active;
1079
+ jobs = /* @__PURE__ */ new Map();
1080
+ queue = [];
1081
+ sequence = 0;
1082
+ idleTimer;
1083
+ getRepoMap(options) {
1084
+ if (options.signal?.aborted) return Promise.reject(new Error("Repository map cancelled", { cause: options.signal.reason }));
1085
+ const root = path10.resolve(options.root);
1086
+ const { signal, ...serializable } = options;
1087
+ const request = { ...serializable, root };
1088
+ const key = JSON.stringify([
1089
+ root,
1090
+ request.files,
1091
+ request.excludePatterns,
1092
+ request.maxLines,
1093
+ request.prepareOnly,
1094
+ [...request.mentionedFiles ?? []].sort(),
1095
+ [...request.mentionedIdents ?? []].sort(),
1096
+ [...request.chatFiles ?? []].sort()
1097
+ ]);
1098
+ let job = this.jobs.get(key);
1099
+ if (!job) {
1100
+ if (this.jobs.size >= 64) return Promise.reject(new Error("Repository map queue is full"));
1101
+ job = {
1102
+ id: ++this.sequence,
1103
+ key,
1104
+ options: request,
1105
+ subscribers: /* @__PURE__ */ new Set()
1106
+ };
1107
+ this.jobs.set(key, job);
1108
+ this.queue.push(job);
1109
+ }
1110
+ const pending = job;
1111
+ return new Promise((resolve5, reject) => {
1112
+ const abort = () => {
1113
+ subscriber.cleanup();
1114
+ pending.subscribers.delete(subscriber);
1115
+ reject(new Error("Repository map cancelled", { cause: signal?.reason }));
1116
+ if (!pending.subscribers.size) {
1117
+ this.jobs.delete(pending.key);
1118
+ this.queue = this.queue.filter((queued) => queued !== pending);
1119
+ if (this.active === pending) {
1120
+ this.stopWorker();
1121
+ this.active = void 0;
1122
+ }
1123
+ this.schedule();
1124
+ }
1125
+ };
1126
+ const subscriber = {
1127
+ resolve: resolve5,
1128
+ reject,
1129
+ cleanup: () => signal?.removeEventListener("abort", abort)
1130
+ };
1131
+ pending.subscribers.add(subscriber);
1132
+ signal?.addEventListener("abort", abort, { once: true });
1133
+ this.schedule();
1134
+ });
1135
+ }
1136
+ schedule() {
1137
+ if (this.active) return;
1138
+ const next = this.queue.shift();
1139
+ if (!next) return;
1140
+ clearTimeout(this.idleTimer);
1141
+ this.active = next;
1142
+ try {
1143
+ if (!this.worker) {
1144
+ const worker = new import_worker_threads.Worker(path10.join(__dirname, "repo-map-worker.js"), {
1145
+ workerData: this.options,
1146
+ resourceLimits: { maxOldGenerationSizeMb: 512 }
1147
+ });
1148
+ this.worker = worker;
1149
+ worker.on("message", (response) => {
1150
+ if (this.worker !== worker || this.active?.id !== response.id) return;
1151
+ this.finish(response);
1152
+ });
1153
+ worker.on("error", (error) => {
1154
+ if (this.worker !== worker) return;
1155
+ this.stopWorker();
1156
+ if (this.active) this.finish({ id: this.active.id, error: error.message });
1157
+ });
1158
+ worker.on("exit", (code) => {
1159
+ if (this.worker !== worker) return;
1160
+ this.worker = void 0;
1161
+ if (this.active)
1162
+ this.finish({
1163
+ id: this.active.id,
1164
+ error: `Repository map worker exited (${code})`
1165
+ });
1166
+ });
1167
+ }
1168
+ this.worker.ref();
1169
+ this.worker.postMessage({
1170
+ id: next.id,
1171
+ options: next.options
1172
+ });
1173
+ } catch (error) {
1174
+ this.stopWorker();
1175
+ this.finish({ id: next.id, error: String(error) });
1176
+ }
1177
+ }
1178
+ finish(response) {
1179
+ const job = this.active;
1180
+ if (!job) return;
1181
+ this.jobs.delete(job.key);
1182
+ this.active = void 0;
1183
+ for (const subscriber of job.subscribers) {
1184
+ subscriber.cleanup();
1185
+ if (response.error !== void 0) subscriber.reject(new Error(response.error));
1186
+ else subscriber.resolve(response.output ?? "");
1187
+ }
1188
+ this.worker?.unref();
1189
+ this.idleTimer = setTimeout(() => this.stopWorker(), 5 * 60 * 1e3);
1190
+ this.idleTimer.unref();
1191
+ this.schedule();
1192
+ }
1193
+ stopWorker() {
1194
+ const worker = this.worker;
1195
+ this.worker = void 0;
1196
+ void worker?.terminate();
1197
+ }
1198
+ dispose() {
1199
+ clearTimeout(this.idleTimer);
1200
+ this.stopWorker();
1201
+ for (const job of this.jobs.values()) {
1202
+ for (const subscriber of job.subscribers) {
1203
+ subscriber.cleanup();
1204
+ subscriber.reject(new Error("Repository map service disposed"));
1205
+ }
1206
+ }
1207
+ this.jobs.clear();
1208
+ this.queue = [];
1209
+ this.active = void 0;
1210
+ }
1211
+ };
1212
+
1213
+ // src/index.ts
1214
+ var repoMapService = new RepoMapService();
1032
1215
  var defaultAnalyzer = null;
1033
1216
  async function getAnalyzer() {
1034
1217
  if (!defaultAnalyzer) {
@@ -1042,14 +1225,14 @@ async function extractSymbols(filePaths, options) {
1042
1225
  return analyzer.extractSymbols(filePaths, options);
1043
1226
  }
1044
1227
  async function getRepoMap(root, options) {
1045
- const analyzer = await getAnalyzer();
1046
- return analyzer.getRepoMap({ ...options, root });
1228
+ return repoMapService.getRepoMap({ ...options, root });
1047
1229
  }
1048
1230
  // Annotate the CommonJS export names for ESM import in node:
1049
1231
  0 && (module.exports = {
1050
1232
  CacheManager,
1051
1233
  GraphBuilder,
1052
1234
  PageRankCalculator,
1235
+ RepoMapService,
1053
1236
  SymbolExtractor,
1054
1237
  TreeRenderer,
1055
1238
  TreeSitterAnalyzer,