@snevins/repo-mapper 1.0.3

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/output.js ADDED
@@ -0,0 +1,135 @@
1
+ import { estimateTokens } from "./tokens.js";
2
+ /**
3
+ * Resolve RankedDefinitions to their Tags by looking up in graph.defsByName.
4
+ * Returns only definitions that can be found.
5
+ */
6
+ function resolveDefs(defs, graph) {
7
+ const result = [];
8
+ for (const def of defs) {
9
+ const tags = graph.defsByName.get(def.ident);
10
+ if (!tags)
11
+ continue;
12
+ const tag = tags.find((t) => t.relPath === def.file);
13
+ if (!tag)
14
+ continue;
15
+ result.push({ file: def.file, tag, rank: def.rank });
16
+ }
17
+ return result;
18
+ }
19
+ /**
20
+ * Group definitions by file path.
21
+ */
22
+ function groupByFile(defs) {
23
+ const groups = new Map();
24
+ for (const def of defs) {
25
+ const existing = groups.get(def.file) ?? [];
26
+ existing.push(def);
27
+ groups.set(def.file, existing);
28
+ }
29
+ return groups;
30
+ }
31
+ /**
32
+ * Format output per spec §3.5.
33
+ * Files sorted by PageRank descending.
34
+ * Definitions within file sorted by line ascending.
35
+ * Line numbers right-aligned to max width.
36
+ */
37
+ export function formatOutput(defs, graph, fileRanks, focusFiles) {
38
+ const resolved = resolveDefs(defs, graph);
39
+ if (resolved.length === 0 && (!focusFiles || focusFiles.length === 0)) {
40
+ return "";
41
+ }
42
+ const groups = groupByFile(resolved);
43
+ // Sort files by PageRank descending
44
+ const sortedFiles = [...groups.keys()].sort((a, b) => {
45
+ const rankA = fileRanks.get(a) ?? 0;
46
+ const rankB = fileRanks.get(b) ?? 0;
47
+ if (rankB !== rankA)
48
+ return rankB - rankA;
49
+ return a.localeCompare(b);
50
+ });
51
+ // Find max line number for padding
52
+ let maxLine = 1;
53
+ for (const def of resolved) {
54
+ if (def.tag.line > maxLine)
55
+ maxLine = def.tag.line;
56
+ }
57
+ const lineWidth = String(maxLine).length;
58
+ const lines = [];
59
+ // Focus files header
60
+ if (focusFiles && focusFiles.length > 0) {
61
+ lines.push(`[Focus: [${focusFiles.join(", ")}]]`);
62
+ lines.push("");
63
+ }
64
+ // Format each file
65
+ for (const file of sortedFiles) {
66
+ const fileDefs = groups.get(file);
67
+ if (!fileDefs || fileDefs.length === 0)
68
+ continue;
69
+ const fileRank = fileRanks.get(file) ?? 0;
70
+ lines.push(`${file}:`);
71
+ lines.push(`(Rank: ${fileRank.toFixed(4)})`);
72
+ lines.push("");
73
+ // Sort definitions by line ascending
74
+ const sorted = [...fileDefs].sort((a, b) => a.tag.line - b.tag.line);
75
+ for (const def of sorted) {
76
+ const lineNum = String(def.tag.line).padStart(lineWidth, " ");
77
+ lines.push(`${lineNum}: ${def.tag.signature ?? def.tag.name}`);
78
+ }
79
+ lines.push("");
80
+ }
81
+ return lines.join("\n");
82
+ }
83
+ /**
84
+ * Fit ranked definitions to token budget using binary search.
85
+ * Returns the largest output that fits within the budget.
86
+ *
87
+ * Algorithm from spec §8.2:
88
+ * 1. Binary search over number of definitions
89
+ * 2. Format first `mid` definitions
90
+ * 3. If tokens ≤ budget, try more; else try fewer
91
+ * 4. Return best fit
92
+ */
93
+ export function fitToTokenBudget(defs, graph, fileRanks, tokenBudget, focusFiles) {
94
+ if (tokenBudget <= 0)
95
+ return "";
96
+ if (defs.length === 0) {
97
+ if (focusFiles && focusFiles.length > 0) {
98
+ const header = `[Focus: [${focusFiles.join(", ")}]]\n\n`;
99
+ return estimateTokens(header) <= tokenBudget ? header : "";
100
+ }
101
+ return "";
102
+ }
103
+ let low = 0;
104
+ let high = defs.length;
105
+ let bestOutput = "";
106
+ let bestCount = 0;
107
+ while (low <= high) {
108
+ const mid = Math.floor((low + high) / 2);
109
+ if (mid === 0) {
110
+ low = 1;
111
+ continue;
112
+ }
113
+ const subset = defs.slice(0, mid);
114
+ const output = formatOutput(subset, graph, fileRanks, focusFiles);
115
+ const tokens = estimateTokens(output);
116
+ if (tokens <= tokenBudget) {
117
+ if (mid > bestCount) {
118
+ bestCount = mid;
119
+ bestOutput = output;
120
+ }
121
+ low = mid + 1;
122
+ }
123
+ else {
124
+ high = mid - 1;
125
+ }
126
+ }
127
+ // If no definitions fit but focus files exist, return just the header
128
+ if (bestOutput === "" && focusFiles && focusFiles.length > 0) {
129
+ const header = `[Focus: [${focusFiles.join(", ")}]]\n\n`;
130
+ if (estimateTokens(header) <= tokenBudget) {
131
+ return header;
132
+ }
133
+ }
134
+ return bestOutput;
135
+ }
@@ -0,0 +1,6 @@
1
+ import type { FileGraph, PageRankOptions } from "./types.js";
2
+ /**
3
+ * Compute PageRank scores for files in the graph.
4
+ * Files with more incoming references get higher scores.
5
+ */
6
+ export declare function computePageRank(graph: FileGraph, options?: PageRankOptions): Map<string, number>;
@@ -0,0 +1,155 @@
1
+ const DEFAULT_DAMPING = 0.85;
2
+ const DEFAULT_THRESHOLD = 1e-6;
3
+ const DEFAULT_MAX_ITERATIONS = 100;
4
+ /**
5
+ * Compute PageRank scores for files in the graph.
6
+ * Files with more incoming references get higher scores.
7
+ */
8
+ export function computePageRank(graph, options) {
9
+ const { nodes, edges, outWeights } = graph;
10
+ const n = nodes.length;
11
+ if (n === 0) {
12
+ return new Map();
13
+ }
14
+ const d = options?.dampingFactor ?? DEFAULT_DAMPING;
15
+ if (d <= 0 || d >= 1) {
16
+ throw new Error(`dampingFactor must be in (0, 1), got ${String(d)}`);
17
+ }
18
+ const threshold = options?.convergenceThreshold ?? DEFAULT_THRESHOLD;
19
+ const maxIter = options?.maxIterations ?? DEFAULT_MAX_ITERATIONS;
20
+ const personalization = options?.personalization;
21
+ // Build node index
22
+ const indexByPath = new Map();
23
+ for (let i = 0; i < n; i++) {
24
+ const node = nodes[i];
25
+ if (node !== undefined) {
26
+ indexByPath.set(node, i);
27
+ }
28
+ }
29
+ // Build personalization vector (normalized)
30
+ const pVec = buildPersonalizationVector(nodes, personalization);
31
+ // Initialize PR from personalization (or uniform)
32
+ const pr = new Float64Array(n);
33
+ for (let i = 0; i < n; i++) {
34
+ pr[i] = pVec[i] ?? 1 / n;
35
+ }
36
+ // Build reverse edges for incoming contribution lookup
37
+ // reverseEdges[to] = array of {from, weight}
38
+ const reverseEdges = buildReverseEdges(nodes, edges, indexByPath);
39
+ // Power iteration
40
+ const next = new Float64Array(n);
41
+ for (let iter = 0; iter < maxIter; iter++) {
42
+ // Base teleport term: (1-d)/N per spec §7.2
43
+ // Uses uniform distribution (not personalized) for teleport
44
+ const base = (1 - d) / n;
45
+ for (let i = 0; i < n; i++) {
46
+ next[i] = base;
47
+ }
48
+ // Compute dangling sum
49
+ let danglingSum = 0;
50
+ for (let i = 0; i < n; i++) {
51
+ const node = nodes[i];
52
+ if (node !== undefined) {
53
+ const out = outWeights.get(node);
54
+ if (out === undefined || out === 0) {
55
+ danglingSum += pr[i] ?? 0;
56
+ }
57
+ }
58
+ }
59
+ // Distribute dangling rank via personalization
60
+ for (let i = 0; i < n; i++) {
61
+ const currentNext = next[i] ?? 0;
62
+ next[i] = currentNext + d * danglingSum * (pVec[i] ?? 1 / n);
63
+ }
64
+ // Add edge contributions
65
+ for (let toIdx = 0; toIdx < n; toIdx++) {
66
+ const incoming = reverseEdges[toIdx];
67
+ if (incoming === undefined)
68
+ continue;
69
+ for (const { fromIdx, weight } of incoming) {
70
+ const fromNode = nodes[fromIdx];
71
+ if (fromNode === undefined)
72
+ continue;
73
+ const fromOut = outWeights.get(fromNode);
74
+ if (fromOut === undefined || fromOut === 0)
75
+ continue;
76
+ const currentNext = next[toIdx] ?? 0;
77
+ next[toIdx] = currentNext + d * (pr[fromIdx] ?? 0) * (weight / fromOut);
78
+ }
79
+ }
80
+ // Check convergence
81
+ let delta = 0;
82
+ for (let i = 0; i < n; i++) {
83
+ delta += Math.abs((next[i] ?? 0) - (pr[i] ?? 0));
84
+ }
85
+ // Swap
86
+ for (let i = 0; i < n; i++) {
87
+ pr[i] = next[i] ?? 0;
88
+ }
89
+ if (delta < threshold) {
90
+ break;
91
+ }
92
+ }
93
+ // Build result map
94
+ const result = new Map();
95
+ for (let i = 0; i < n; i++) {
96
+ const node = nodes[i];
97
+ if (node !== undefined) {
98
+ result.set(node, pr[i] ?? 0);
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function buildPersonalizationVector(nodes, personalization) {
104
+ const n = nodes.length;
105
+ const pVec = new Float64Array(n);
106
+ if (personalization === undefined || personalization.size === 0) {
107
+ // Uniform
108
+ const uniform = 1 / n;
109
+ for (let i = 0; i < n; i++) {
110
+ pVec[i] = uniform;
111
+ }
112
+ return pVec;
113
+ }
114
+ // Build from personalization map
115
+ let sum = 0;
116
+ for (let i = 0; i < n; i++) {
117
+ const node = nodes[i];
118
+ if (node !== undefined) {
119
+ const weight = personalization.get(node) ?? 0;
120
+ pVec[i] = weight;
121
+ sum += weight;
122
+ }
123
+ }
124
+ // Normalize
125
+ if (sum > 0) {
126
+ for (let i = 0; i < n; i++) {
127
+ const current = pVec[i] ?? 0;
128
+ pVec[i] = current / sum;
129
+ }
130
+ }
131
+ else {
132
+ // Fallback to uniform if all weights are 0
133
+ const uniform = 1 / n;
134
+ for (let i = 0; i < n; i++) {
135
+ pVec[i] = uniform;
136
+ }
137
+ }
138
+ return pVec;
139
+ }
140
+ function buildReverseEdges(nodes, edges, indexByPath) {
141
+ const n = nodes.length;
142
+ const reverseEdges = Array.from({ length: n }, () => []);
143
+ for (const [from, toMap] of edges) {
144
+ const fromIdx = indexByPath.get(from);
145
+ if (fromIdx === undefined)
146
+ continue;
147
+ for (const [to, weight] of toMap) {
148
+ const toIdx = indexByPath.get(to);
149
+ if (toIdx === undefined)
150
+ continue;
151
+ reverseEdges[toIdx]?.push({ fromIdx, weight });
152
+ }
153
+ }
154
+ return reverseEdges;
155
+ }
@@ -0,0 +1,31 @@
1
+ import type { Tag } from "./types.js";
2
+ import { type LanguageConfig } from "./languages.js";
3
+ /**
4
+ * Maximum tags per file. Files exceeding this are likely bundled/generated.
5
+ * Skip them to avoid cache shard overflow.
6
+ */
7
+ export declare const MAX_TAGS_PER_FILE = 1000;
8
+ /**
9
+ * Initialize the tree-sitter parser.
10
+ * Must be called before parseFile.
11
+ */
12
+ export declare function initParser(): Promise<void>;
13
+ /**
14
+ * Cleanup parser resources.
15
+ */
16
+ export declare function shutdownParser(): void;
17
+ /**
18
+ * Parse a single file and extract tags.
19
+ */
20
+ export declare function parseFile(absPath: string, relPath: string): Promise<readonly Tag[]>;
21
+ /**
22
+ * Extract definitions using regex patterns as fallback.
23
+ * Used when tree-sitter parsing yields no definitions.
24
+ * @param content - File content to parse
25
+ * @param patterns - Regex patterns with name captured in group 1
26
+ * @param relPath - Relative path for Tag
27
+ * @param absPath - Absolute path for Tag
28
+ * @param config - Language config for builtin filtering
29
+ * @returns Array of definition Tags
30
+ */
31
+ export declare function extractDefsWithRegex(content: string, patterns: readonly RegExp[], relPath: string, absPath: string, config?: LanguageConfig): Tag[];
package/dist/parser.js ADDED
@@ -0,0 +1,344 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { extname, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Parser, Language } from "web-tree-sitter";
5
+ import { getLanguageForExtension, isBuiltin, LANGUAGE_REGISTRY } from "./languages.js";
6
+ let parser;
7
+ const loadedLanguages = new Map();
8
+ /**
9
+ * Maximum tags per file. Files exceeding this are likely bundled/generated.
10
+ * Skip them to avoid cache shard overflow.
11
+ */
12
+ export const MAX_TAGS_PER_FILE = 1000;
13
+ /**
14
+ * Get node_modules path relative to this file.
15
+ */
16
+ function getNodeModulesPath() {
17
+ const currentDir = dirname(fileURLToPath(import.meta.url));
18
+ const projectRoot = resolve(currentDir, "..");
19
+ return resolve(projectRoot, "node_modules");
20
+ }
21
+ /**
22
+ * Initialize the tree-sitter parser.
23
+ * Must be called before parseFile.
24
+ */
25
+ export async function initParser() {
26
+ if (parser)
27
+ return;
28
+ await Parser.init();
29
+ parser = new Parser();
30
+ }
31
+ /**
32
+ * Cleanup parser resources.
33
+ */
34
+ export function shutdownParser() {
35
+ parser = undefined;
36
+ loadedLanguages.clear();
37
+ }
38
+ /**
39
+ * Load a language grammar, caching for reuse.
40
+ * Throws descriptive error if WASM file cannot be loaded.
41
+ */
42
+ async function loadLanguage(config) {
43
+ const grammarName = config.grammarName;
44
+ const cached = loadedLanguages.get(grammarName);
45
+ if (cached)
46
+ return cached;
47
+ const nodeModules = getNodeModulesPath();
48
+ const wasmPath = config.wasmPath(nodeModules);
49
+ try {
50
+ const language = await Language.load(wasmPath);
51
+ loadedLanguages.set(grammarName, language);
52
+ return language;
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`Failed to load ${grammarName} grammar from ${wasmPath}: ${message}`);
57
+ }
58
+ }
59
+ /**
60
+ * Combined definition node types from all languages.
61
+ * Derived from LANGUAGE_REGISTRY to ensure single source of truth.
62
+ */
63
+ const ALL_DEFINITION_TYPES = new Set(Object.values(LANGUAGE_REGISTRY).flatMap((config) => [...config.definitionTypes]));
64
+ /**
65
+ * Check if a node is inside an export statement (JS/TS).
66
+ */
67
+ function isExported(node) {
68
+ let current = node.parent;
69
+ while (current) {
70
+ if (current.type === "export_statement")
71
+ return true;
72
+ if (current.type === "program")
73
+ return false;
74
+ current = current.parent;
75
+ }
76
+ return false;
77
+ }
78
+ /**
79
+ * Check if definition should be included based on language rules.
80
+ * Different languages have different visibility rules.
81
+ */
82
+ function shouldIncludeDefinition(node, config) {
83
+ const grammarName = config.grammarName;
84
+ // Python: Include all top-level definitions (no export keyword)
85
+ if (grammarName === "python") {
86
+ return true;
87
+ }
88
+ // Go: Include all top-level definitions (capitalization = export, but we include all)
89
+ if (grammarName === "go") {
90
+ return true;
91
+ }
92
+ // Rust: Include pub items and all top-level items
93
+ if (grammarName === "rust") {
94
+ return true;
95
+ }
96
+ // Solidity: Include all definitions
97
+ if (grammarName === "solidity") {
98
+ return true;
99
+ }
100
+ // TypeScript/JavaScript: Require export or special types
101
+ return (node.type === "method_definition" ||
102
+ node.type === "interface_declaration" ||
103
+ node.type === "type_alias_declaration" ||
104
+ node.type === "enum_declaration" ||
105
+ isExported(node));
106
+ }
107
+ /**
108
+ * Get the name from a definition node.
109
+ */
110
+ function getDefinitionName(node) {
111
+ // For most declarations, look for 'name' or 'identifier' child
112
+ const nameNode = node.childForFieldName("name");
113
+ if (nameNode)
114
+ return nameNode.text;
115
+ // For method_definition, name is the first identifier child
116
+ if (node.type === "method_definition") {
117
+ const identNode = node.children.find((c) => c.type === "property_identifier");
118
+ return identNode?.text;
119
+ }
120
+ return undefined;
121
+ }
122
+ /**
123
+ * Get the name from a variable declarator.
124
+ */
125
+ function getVariableDeclaratorName(node) {
126
+ const nameNode = node.childForFieldName("name");
127
+ if (nameNode?.type === "identifier")
128
+ return nameNode.text;
129
+ return undefined;
130
+ }
131
+ /**
132
+ * Get the first line of a node as signature.
133
+ */
134
+ function getSignature(node, sourceLines) {
135
+ const lineIndex = node.startPosition.row;
136
+ const line = sourceLines[lineIndex];
137
+ return line ?? "";
138
+ }
139
+ /**
140
+ * Check if identifier is in definition position (not a reference).
141
+ */
142
+ function isIdentifierInDefinitionPosition(node) {
143
+ const parent = node.parent;
144
+ if (!parent)
145
+ return false;
146
+ // Check if this identifier is the 'name' field of a definition
147
+ if (ALL_DEFINITION_TYPES.has(parent.type)) {
148
+ const nameNode = parent.childForFieldName("name");
149
+ return nameNode === node;
150
+ }
151
+ // Check for variable declarator name
152
+ if (parent.type === "variable_declarator") {
153
+ const nameNode = parent.childForFieldName("name");
154
+ return nameNode === node;
155
+ }
156
+ // Check for parameter names (TypeScript/JavaScript)
157
+ if (parent.type === "required_parameter" || parent.type === "optional_parameter") {
158
+ const nameNode = parent.childForFieldName("pattern") ?? parent.childForFieldName("name");
159
+ return nameNode === node;
160
+ }
161
+ // Check for shorthand property identifier (in object patterns)
162
+ if (parent.type === "shorthand_property_identifier_pattern") {
163
+ return true;
164
+ }
165
+ // Check for method name
166
+ if (parent.type === "method_definition") {
167
+ return true;
168
+ }
169
+ // Check for property name in object
170
+ if (parent.type === "pair" || parent.type === "property_signature") {
171
+ const keyNode = parent.childForFieldName("key") ?? parent.childForFieldName("name");
172
+ return keyNode === node;
173
+ }
174
+ // Check for import specifier
175
+ if (parent.type === "import_specifier") {
176
+ return true;
177
+ }
178
+ // Check for type parameter
179
+ if (parent.type === "type_parameter") {
180
+ return true;
181
+ }
182
+ // Python: function/class parameters
183
+ if (parent.type === "parameters" || parent.type === "typed_parameter" || parent.type === "default_parameter") {
184
+ return true;
185
+ }
186
+ // Go: parameter declarations
187
+ if (parent.type === "parameter_declaration" || parent.type === "variadic_parameter_declaration") {
188
+ return true;
189
+ }
190
+ // Rust: pattern bindings
191
+ if (parent.type === "parameter" || parent.type === "let_declaration") {
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+ /**
197
+ * Parse a single file and extract tags.
198
+ */
199
+ export async function parseFile(absPath, relPath) {
200
+ if (!parser) {
201
+ throw new Error("Parser not initialized. Call initParser() first.");
202
+ }
203
+ const ext = extname(absPath);
204
+ const config = getLanguageForExtension(ext);
205
+ // Return empty for unsupported extensions
206
+ if (!config)
207
+ return [];
208
+ let source;
209
+ try {
210
+ source = await readFile(absPath, "utf-8");
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ // Empty file
216
+ if (!source.trim())
217
+ return [];
218
+ const language = await loadLanguage(config);
219
+ parser.setLanguage(language);
220
+ const tree = parser.parse(source);
221
+ if (!tree)
222
+ return [];
223
+ const sourceLines = source.split("\n");
224
+ const tags = [];
225
+ // Capture config in a const for closure (TypeScript narrowing)
226
+ const langConfig = config;
227
+ // Walk tree to extract definitions and references
228
+ const cursor = tree.walk();
229
+ const visitedNodes = new Set();
230
+ function walk() {
231
+ const node = cursor.currentNode;
232
+ const nodeId = node.id;
233
+ // Avoid visiting same node twice
234
+ if (visitedNodes.has(nodeId))
235
+ return;
236
+ visitedNodes.add(nodeId);
237
+ // Check for definition nodes using config's definition types
238
+ if (langConfig.definitionTypes.has(node.type)) {
239
+ if (shouldIncludeDefinition(node, langConfig)) {
240
+ const name = getDefinitionName(node);
241
+ if (name && !isBuiltin(name, langConfig)) {
242
+ tags.push({
243
+ relPath,
244
+ absPath,
245
+ line: node.startPosition.row + 1, // 1-indexed
246
+ name,
247
+ kind: "def",
248
+ signature: getSignature(node, sourceLines),
249
+ });
250
+ }
251
+ }
252
+ }
253
+ // Check for exported variable declarators (const/let) - TypeScript/JavaScript only
254
+ if (node.type === "variable_declarator" && isExported(node)) {
255
+ const name = getVariableDeclaratorName(node);
256
+ if (name && !isBuiltin(name, langConfig)) {
257
+ tags.push({
258
+ relPath,
259
+ absPath,
260
+ line: node.startPosition.row + 1,
261
+ name,
262
+ kind: "def",
263
+ signature: getSignature(node, sourceLines),
264
+ });
265
+ }
266
+ }
267
+ // Check for identifier references
268
+ if (node.type === "identifier" || node.type === "type_identifier") {
269
+ const name = node.text;
270
+ if (!isBuiltin(name, langConfig) && !isIdentifierInDefinitionPosition(node)) {
271
+ tags.push({
272
+ relPath,
273
+ absPath,
274
+ line: node.startPosition.row + 1,
275
+ name,
276
+ kind: "ref",
277
+ });
278
+ }
279
+ }
280
+ // Recurse into children
281
+ if (cursor.gotoFirstChild()) {
282
+ do {
283
+ walk();
284
+ } while (cursor.gotoNextSibling());
285
+ cursor.gotoParent();
286
+ }
287
+ }
288
+ walk();
289
+ tree.delete();
290
+ // If tree-sitter found no definitions, try regex fallback
291
+ const defs = tags.filter((t) => t.kind === "def");
292
+ if (defs.length === 0 && langConfig.fallbackPatterns.length > 0) {
293
+ const fallbackDefs = extractDefsWithRegex(source, langConfig.fallbackPatterns, relPath, absPath, langConfig);
294
+ tags.push(...fallbackDefs);
295
+ }
296
+ // Skip files with too many tags (likely bundled/generated)
297
+ if (tags.length > MAX_TAGS_PER_FILE) {
298
+ return [];
299
+ }
300
+ return tags;
301
+ }
302
+ /**
303
+ * Extract definitions using regex patterns as fallback.
304
+ * Used when tree-sitter parsing yields no definitions.
305
+ * @param content - File content to parse
306
+ * @param patterns - Regex patterns with name captured in group 1
307
+ * @param relPath - Relative path for Tag
308
+ * @param absPath - Absolute path for Tag
309
+ * @param config - Language config for builtin filtering
310
+ * @returns Array of definition Tags
311
+ */
312
+ export function extractDefsWithRegex(content, patterns, relPath, absPath, config) {
313
+ const tags = [];
314
+ const lines = content.split("\n");
315
+ for (const pattern of patterns) {
316
+ // Create regex with global and multiline flags, preserving original flags
317
+ const flags = new Set(pattern.flags.split(""));
318
+ flags.add("g");
319
+ flags.add("m");
320
+ const globalPattern = new RegExp(pattern.source, [...flags].join(""));
321
+ let match;
322
+ while ((match = globalPattern.exec(content)) !== null) {
323
+ const name = match[1];
324
+ if (!name)
325
+ continue;
326
+ // Filter builtins for consistency with tree-sitter path
327
+ if (isBuiltin(name, config))
328
+ continue;
329
+ // Calculate line number from match index
330
+ const textBefore = content.slice(0, match.index);
331
+ const lineNumber = textBefore.split("\n").length;
332
+ const signature = lines[lineNumber - 1] ?? "";
333
+ tags.push({
334
+ relPath,
335
+ absPath,
336
+ line: lineNumber,
337
+ name,
338
+ kind: "def",
339
+ signature,
340
+ });
341
+ }
342
+ }
343
+ return tags;
344
+ }
@@ -0,0 +1,13 @@
1
+ import type { FileGraph, RankedDefinition } from "./types.js";
2
+ /**
3
+ * Build personalization vector for focus files.
4
+ * Each focus file gets weight 1.0.
5
+ */
6
+ export declare function buildPersonalization(focusFiles: readonly string[]): Map<string, number>;
7
+ /**
8
+ * Rank definitions by propagating PageRank through symbol edges.
9
+ * Focus file definitions are excluded from output.
10
+ *
11
+ * Formula: def_rank[definer:ident] += PR(referencer) * edge_weight / out_weight(referencer)
12
+ */
13
+ export declare function rankDefinitions(graph: FileGraph, fileRanks: Map<string, number>, focusFiles?: ReadonlySet<string>): RankedDefinition[];