universal-ast-mapper 2.0.0 → 2.0.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +185 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +341 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
@@ -0,0 +1,264 @@
1
+ import path from "node:path";
2
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
3
+ /** Replace non-word characters with underscore for Mermaid identifiers. */
4
+ function sanitizeName(name) {
5
+ return name.replace(/\W/g, "_");
6
+ }
7
+ /** Replace path separators, dots, and dashes with underscore for Mermaid node IDs. */
8
+ function sanitizeId(filePath) {
9
+ return filePath.replace(/[/.\-]/g, "_");
10
+ }
11
+ // ─── Class Diagram ────────────────────────────────────────────────────────────
12
+ /**
13
+ * Class diagram from skeletons — shows classes, interfaces, enums and their
14
+ * relationships.
15
+ */
16
+ export function buildClassDiagram(skeletons) {
17
+ const lines = ["classDiagram"];
18
+ const entries = [];
19
+ for (const skel of skeletons) {
20
+ for (const sym of skel.symbols) {
21
+ if (sym.kind === "class" || sym.kind === "interface" || sym.kind === "enum") {
22
+ entries.push({
23
+ name: sym.name,
24
+ sanitized: sanitizeName(sym.name),
25
+ kind: sym.kind,
26
+ node: sym,
27
+ });
28
+ }
29
+ }
30
+ }
31
+ // Enforce the 40-class limit.
32
+ const MAX_CLASSES = 40;
33
+ const truncated = entries.length > MAX_CLASSES;
34
+ const visible = truncated ? entries.slice(0, MAX_CLASSES) : entries;
35
+ const visibleNames = new Set(visible.map((e) => e.name));
36
+ let edgeCount = 0;
37
+ // Emit class/interface/enum blocks.
38
+ for (const entry of visible) {
39
+ const { sanitized, kind, node } = entry;
40
+ if (kind === "interface") {
41
+ lines.push(` class ${sanitized} {`);
42
+ lines.push(` <<interface>>`);
43
+ for (const child of node.children) {
44
+ if (child.kind === "method" || child.kind === "function") {
45
+ const prefix = child.visibility === "private" ? "-" : "+";
46
+ lines.push(` ${prefix}${sanitizeName(child.name)}()`);
47
+ }
48
+ }
49
+ lines.push(` }`);
50
+ }
51
+ else if (kind === "enum") {
52
+ lines.push(` class ${sanitized} {`);
53
+ lines.push(` <<enumeration>>`);
54
+ for (const child of node.children) {
55
+ lines.push(` ${sanitizeName(child.name)}`);
56
+ }
57
+ lines.push(` }`);
58
+ }
59
+ else {
60
+ // class
61
+ lines.push(` class ${sanitized} {`);
62
+ for (const child of node.children) {
63
+ const prefix = child.visibility === "private" ? "-" : "+";
64
+ if (child.kind === "method" || child.kind === "function") {
65
+ lines.push(` ${prefix}${sanitizeName(child.name)}()`);
66
+ }
67
+ else if (child.kind === "field") {
68
+ // Try to extract type from signature
69
+ let fieldType = "";
70
+ if (child.signature) {
71
+ // Signature format: "fieldName: TypeName" or "fieldName?: TypeName"
72
+ const match = child.signature.match(/:\s*(.+)$/);
73
+ if (match)
74
+ fieldType = sanitizeName(match[1].trim().split(/\s/)[0]) + " ";
75
+ }
76
+ lines.push(` ${prefix}${fieldType}${sanitizeName(child.name)}`);
77
+ }
78
+ }
79
+ lines.push(` }`);
80
+ }
81
+ }
82
+ // Emit "uses" edges from imports.
83
+ for (const skel of skeletons) {
84
+ if (!skel.imports)
85
+ continue;
86
+ // Find the class/interface names in this file.
87
+ const fileClasses = skel.symbols
88
+ .filter((s) => s.kind === "class" || s.kind === "interface" || s.kind === "enum")
89
+ .map((s) => s.name);
90
+ for (const fileClass of fileClasses) {
91
+ if (!visibleNames.has(fileClass))
92
+ continue;
93
+ const from = sanitizeName(fileClass);
94
+ for (const imp of skel.imports) {
95
+ if (imp.symbol && imp.symbol !== "*" && visibleNames.has(imp.symbol)) {
96
+ const to = sanitizeName(imp.symbol);
97
+ if (from !== to) {
98
+ lines.push(` ${from} --> ${to} : uses`);
99
+ edgeCount++;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ if (truncated) {
106
+ lines.push(` %% ... and ${entries.length - MAX_CLASSES} more`);
107
+ }
108
+ return {
109
+ type: "class",
110
+ mermaid: lines.join("\n"),
111
+ title: "Class Diagram",
112
+ nodeCount: visible.length,
113
+ edgeCount,
114
+ };
115
+ }
116
+ // ─── Deps Diagram ─────────────────────────────────────────────────────────────
117
+ /**
118
+ * File dependency diagram from symbol graph.
119
+ */
120
+ export function buildDepsDiagram(graph, maxNodes = 50) {
121
+ const lines = ["graph TD"];
122
+ // Collect file nodes only.
123
+ const fileNodes = graph.nodes.filter((n) => n.nodeType === "file");
124
+ const truncated = fileNodes.length > maxNodes;
125
+ const visibleFiles = new Set((truncated ? fileNodes.slice(0, maxNodes) : fileNodes).map((n) => n.id));
126
+ // Emit node labels.
127
+ for (const n of fileNodes) {
128
+ if (!visibleFiles.has(n.id))
129
+ continue;
130
+ const nodeId = sanitizeId(n.id);
131
+ const label = path.basename(n.id, path.extname(n.id));
132
+ lines.push(` ${nodeId}["${label}"]`);
133
+ lines.push(` click ${nodeId} callback "${n.id}"`);
134
+ }
135
+ // Collect import edges between visible file nodes.
136
+ // Build a map: symbolNodeId → file it belongs to.
137
+ const symbolToFile = new Map();
138
+ for (const n of graph.nodes) {
139
+ if (n.nodeType === "symbol") {
140
+ const sym = n;
141
+ symbolToFile.set(sym.id, sym.file);
142
+ }
143
+ }
144
+ // Deduplicate file-level edges.
145
+ const emittedEdges = new Set();
146
+ let edgeCount = 0;
147
+ for (const edge of graph.edges) {
148
+ if (edge.edgeType !== "imports")
149
+ continue;
150
+ const fromFile = edge.from;
151
+ // `to` may be a symbol node ID or a file node ID.
152
+ const toFile = symbolToFile.get(edge.to) ?? edge.to;
153
+ if (fromFile === toFile)
154
+ continue;
155
+ if (!visibleFiles.has(fromFile) || !visibleFiles.has(toFile))
156
+ continue;
157
+ const key = `${fromFile}→${toFile}`;
158
+ if (emittedEdges.has(key))
159
+ continue;
160
+ emittedEdges.add(key);
161
+ const fromId = sanitizeId(fromFile);
162
+ const toId = sanitizeId(toFile);
163
+ lines.push(` ${fromId} --> ${toId}`);
164
+ edgeCount++;
165
+ }
166
+ if (truncated) {
167
+ lines.push(` %% ... ${fileNodes.length - maxNodes} more nodes not shown`);
168
+ }
169
+ return {
170
+ type: "deps",
171
+ mermaid: lines.join("\n"),
172
+ title: "File Dependency Diagram",
173
+ nodeCount: visibleFiles.size,
174
+ edgeCount,
175
+ };
176
+ }
177
+ // ─── Modules Diagram ──────────────────────────────────────────────────────────
178
+ /**
179
+ * Module/directory-level dependency diagram (collapsed by top-level directory).
180
+ */
181
+ export function buildModulesDiagram(graph) {
182
+ const MAX_MODULES = 20;
183
+ const lines = ["graph LR"];
184
+ // Map each file to its top-level module (first directory component).
185
+ function fileToModule(fileId) {
186
+ const parts = fileId.split("/");
187
+ // If the file is at the root level (no directory), use "." as the module.
188
+ if (parts.length <= 1)
189
+ return ".";
190
+ // Use up to 2 path components for meaningful grouping when first segment is short
191
+ // (e.g. "src/auth" for "src/auth/user.ts").
192
+ return parts.slice(0, Math.min(2, parts.length - 1)).join("/");
193
+ }
194
+ // Build symbol-to-file lookup.
195
+ const symbolToFile = new Map();
196
+ for (const n of graph.nodes) {
197
+ if (n.nodeType === "symbol") {
198
+ const sym = n;
199
+ symbolToFile.set(sym.id, sym.file);
200
+ }
201
+ }
202
+ // Count inter-module edges.
203
+ const moduleEdgeCounts = new Map();
204
+ const moduleSet = new Set();
205
+ for (const n of graph.nodes) {
206
+ if (n.nodeType === "file") {
207
+ moduleSet.add(fileToModule(n.id));
208
+ }
209
+ }
210
+ for (const edge of graph.edges) {
211
+ if (edge.edgeType !== "imports")
212
+ continue;
213
+ const fromFile = edge.from;
214
+ const toFile = symbolToFile.get(edge.to) ?? edge.to;
215
+ if (fromFile === toFile)
216
+ continue;
217
+ const fromModule = fileToModule(fromFile);
218
+ const toModule = fileToModule(toFile);
219
+ if (fromModule === toModule)
220
+ continue;
221
+ const key = `${fromModule}→${toModule}`;
222
+ moduleEdgeCounts.set(key, (moduleEdgeCounts.get(key) ?? 0) + 1);
223
+ }
224
+ // Determine which modules are actually referenced by edges, then sort by
225
+ // occurrence to pick the most connected ones if we need to truncate.
226
+ const moduleOccurrences = new Map();
227
+ for (const [key, count] of moduleEdgeCounts) {
228
+ const [from, to] = key.split("→");
229
+ moduleOccurrences.set(from, (moduleOccurrences.get(from) ?? 0) + count);
230
+ moduleOccurrences.set(to, (moduleOccurrences.get(to) ?? 0) + count);
231
+ }
232
+ // Include all modules from the file set, sorted by occurrence descending.
233
+ const allModules = [...moduleSet].sort((a, b) => {
234
+ return (moduleOccurrences.get(b) ?? 0) - (moduleOccurrences.get(a) ?? 0);
235
+ });
236
+ const truncated = allModules.length > MAX_MODULES;
237
+ const visibleModules = new Set(truncated ? allModules.slice(0, MAX_MODULES) : allModules);
238
+ // Emit module nodes.
239
+ for (const mod of visibleModules) {
240
+ const nodeId = sanitizeName(mod);
241
+ lines.push(` ${nodeId}["${mod}"]`);
242
+ }
243
+ // Emit weighted edges between visible modules.
244
+ let edgeCount = 0;
245
+ for (const [key, count] of moduleEdgeCounts) {
246
+ const [fromModule, toModule] = key.split("→");
247
+ if (!visibleModules.has(fromModule) || !visibleModules.has(toModule))
248
+ continue;
249
+ const fromId = sanitizeName(fromModule);
250
+ const toId = sanitizeName(toModule);
251
+ lines.push(` ${fromId} -->|${count}| ${toId}`);
252
+ edgeCount++;
253
+ }
254
+ if (truncated) {
255
+ lines.push(` %% ... ${allModules.length - MAX_MODULES} more modules not shown`);
256
+ }
257
+ return {
258
+ type: "modules",
259
+ mermaid: lines.join("\n"),
260
+ title: "Module Dependency Diagram",
261
+ nodeCount: visibleModules.size,
262
+ edgeCount,
263
+ };
264
+ }
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ // ─── Persistent (on-disk) parse cache ─────────────────────────────────────────
5
+ // Content-hash keyed: the key embeds schemaVersion + grammar source + detail +
6
+ // the file's raw bytes, so entries never go stale — a changed file simply maps
7
+ // to a new key. Stored as sharded JSON files under <root>/.ast-map/cache.
8
+ // Enabled by calling initDiskCache() once at startup (CLI / MCP server / worker).
9
+ let cacheDir = null;
10
+ /** Enable (or disable with null) the disk cache for this process. */
11
+ export function initDiskCache(dir) {
12
+ cacheDir = dir;
13
+ }
14
+ /** The currently active cache directory, or null when disabled. */
15
+ export function diskCacheDir() {
16
+ return cacheDir;
17
+ }
18
+ /** Conventional cache location for a project root. */
19
+ export function defaultCacheDir(root) {
20
+ return path.join(root, ".ast-map", "cache");
21
+ }
22
+ /** Stable cache key for a (source, detail, schema, grammar) tuple. */
23
+ export function diskCacheKey(source, detail, schemaVersion, grammarSource) {
24
+ return crypto
25
+ .createHash("sha1")
26
+ .update(`${schemaVersion}\0${grammarSource}\0${detail}\0`)
27
+ .update(source)
28
+ .digest("hex");
29
+ }
30
+ function shardPath(dir, key) {
31
+ return path.join(dir, key.slice(0, 2), key.slice(2) + ".json");
32
+ }
33
+ /** Read a cached skeleton, or null on miss / disabled / corrupt entry. */
34
+ export function diskCacheGet(key) {
35
+ if (!cacheDir)
36
+ return null;
37
+ try {
38
+ const raw = fs.readFileSync(shardPath(cacheDir, key), "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ return parsed.skel ?? null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Persist a skeleton under the given key (best-effort, never throws). */
47
+ export function diskCachePut(key, skel) {
48
+ if (!cacheDir)
49
+ return;
50
+ try {
51
+ const file = shardPath(cacheDir, key);
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ const tmp = file + "." + process.pid + ".tmp";
54
+ fs.writeFileSync(tmp, JSON.stringify({ skel }));
55
+ fs.renameSync(tmp, file);
56
+ }
57
+ catch {
58
+ /* cache write failures are non-fatal */
59
+ }
60
+ }
61
+ /** Count entries + total size of a cache directory. */
62
+ export function diskCacheStats(dir) {
63
+ let entries = 0;
64
+ let bytes = 0;
65
+ const walk = (d) => {
66
+ let names = [];
67
+ try {
68
+ names = fs.readdirSync(d, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ return;
72
+ }
73
+ for (const e of names) {
74
+ const p = path.join(d, e.name);
75
+ if (e.isDirectory())
76
+ walk(p);
77
+ else if (e.name.endsWith(".json")) {
78
+ entries++;
79
+ try {
80
+ bytes += fs.statSync(p).size;
81
+ }
82
+ catch { /* skip */ }
83
+ }
84
+ }
85
+ };
86
+ walk(dir);
87
+ return { dir, entries, bytes };
88
+ }
89
+ /** Remove every entry in a cache directory. Returns how many were removed. */
90
+ export function clearDiskCache(dir) {
91
+ const { entries } = diskCacheStats(dir);
92
+ try {
93
+ fs.rmSync(dir, { recursive: true, force: true });
94
+ }
95
+ catch { /* best-effort */ }
96
+ return entries;
97
+ }
package/dist/docgen.js ADDED
@@ -0,0 +1,156 @@
1
+ import https from "node:https";
2
+ function flattenSymbols(symbols, file) {
3
+ const result = [];
4
+ for (const sym of symbols) {
5
+ result.push({
6
+ file,
7
+ name: sym.name,
8
+ kind: sym.kind,
9
+ signature: sym.signature,
10
+ exported: sym.exported !== false,
11
+ lineStart: sym.range.startLine,
12
+ description: sym.doc ?? undefined,
13
+ });
14
+ if (sym.children.length > 0) {
15
+ result.push(...flattenSymbols(sym.children, file));
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ export function buildDocOutput(skeletons, opts = {}) {
21
+ const files = [];
22
+ let totalSymbols = 0;
23
+ let exportedSymbols = 0;
24
+ for (const skel of skeletons) {
25
+ let syms = flattenSymbols(skel.symbols, skel.file);
26
+ if (opts.exportedOnly)
27
+ syms = syms.filter(s => s.exported);
28
+ if (syms.length === 0)
29
+ continue;
30
+ totalSymbols += syms.length;
31
+ exportedSymbols += syms.filter(s => s.exported).length;
32
+ files.push({ file: skel.file, language: skel.language, symbols: syms });
33
+ }
34
+ return { files, totalSymbols, exportedSymbols };
35
+ }
36
+ function htmlEsc(s) {
37
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
38
+ }
39
+ export function renderMarkdown(output) {
40
+ const lines = [
41
+ "# API Reference",
42
+ "",
43
+ `> ${output.exportedSymbols} exported symbols across ${output.files.length} files`,
44
+ "",
45
+ ];
46
+ for (const f of output.files) {
47
+ lines.push(`## \`${f.file}\``, "");
48
+ for (const sym of f.symbols) {
49
+ const expMark = sym.exported ? "" : " _(internal)_";
50
+ lines.push(`### \`${sym.name}\` _(${sym.kind})_${expMark}`, "");
51
+ if (sym.signature)
52
+ lines.push("```", sym.signature, "```", "");
53
+ if (sym.description)
54
+ lines.push(sym.description, "");
55
+ lines.push(`_Line ${sym.lineStart}_`, "");
56
+ }
57
+ }
58
+ return lines.join("\n");
59
+ }
60
+ export function renderDocHtml(output) {
61
+ const rows = output.files.flatMap(f => f.symbols.map(s => `<tr><td><code>${htmlEsc(f.file)}</code></td><td><code>${htmlEsc(s.name)}</code></td><td>${s.kind}</td><td>${s.exported ? "✓" : ""}</td><td><code>${htmlEsc(s.signature ?? "")}</code></td><td>${htmlEsc(s.description ?? "")}</td></tr>`)).join("\n");
62
+ return `<!DOCTYPE html>
63
+ <html lang="en">
64
+ <head><meta charset="utf-8"><title>API Reference</title>
65
+ <style>body{font-family:sans-serif;padding:2rem;max-width:1200px;margin:0 auto}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;vertical-align:top}th{background:#f5f5f5;font-size:.8em;text-transform:uppercase}code{font-family:monospace;font-size:.9em;background:#f0f0f0;padding:1px 3px;border-radius:2px}h1{margin-bottom:4px}p{color:#666;margin-bottom:1.5rem}</style>
66
+ </head>
67
+ <body>
68
+ <h1>API Reference</h1>
69
+ <p>${output.exportedSymbols} exported symbols &bull; ${output.files.length} files</p>
70
+ <table>
71
+ <thead><tr><th>File</th><th>Name</th><th>Kind</th><th>Exp</th><th>Signature</th><th>Description</th></tr></thead>
72
+ <tbody>
73
+ ${rows}
74
+ </tbody>
75
+ </table>
76
+ </body></html>`;
77
+ }
78
+ // ─── AI enhancement ───────────────────────────────────────────────────────────
79
+ async function callClaude(prompt, opts) {
80
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
81
+ if (!apiKey)
82
+ throw new Error("No Anthropic API key — set ANTHROPIC_API_KEY or pass --api-key");
83
+ const body = JSON.stringify({
84
+ model: opts.model ?? "claude-sonnet-4-6",
85
+ max_tokens: opts.maxTokens ?? 1024,
86
+ messages: [{ role: "user", content: prompt }],
87
+ });
88
+ return new Promise((resolve, reject) => {
89
+ const req = https.request({
90
+ hostname: "api.anthropic.com",
91
+ path: "/v1/messages",
92
+ method: "POST",
93
+ headers: {
94
+ "content-type": "application/json",
95
+ "x-api-key": apiKey,
96
+ "anthropic-version": "2023-06-01",
97
+ "content-length": Buffer.byteLength(body),
98
+ },
99
+ }, (res) => {
100
+ const chunks = [];
101
+ res.on("data", (c) => chunks.push(c));
102
+ res.on("end", () => {
103
+ const raw = Buffer.concat(chunks).toString("utf8");
104
+ try {
105
+ const parsed = JSON.parse(raw);
106
+ if (parsed.error)
107
+ reject(new Error(`Anthropic API: ${parsed.error.message}`));
108
+ else
109
+ resolve(parsed.content?.[0]?.text ?? "");
110
+ }
111
+ catch {
112
+ reject(new Error("Unexpected API response"));
113
+ }
114
+ });
115
+ });
116
+ req.on("error", reject);
117
+ req.write(body);
118
+ req.end();
119
+ });
120
+ }
121
+ export async function aiEnhanceDocs(output, opts = {}) {
122
+ const enhanced = {
123
+ ...output,
124
+ files: output.files.map(f => ({ ...f, symbols: f.symbols.map(s => ({ ...s })) })),
125
+ };
126
+ for (const docFile of enhanced.files) {
127
+ const toEnhance = docFile.symbols.filter(s => s.exported && !s.description);
128
+ if (toEnhance.length === 0)
129
+ continue;
130
+ const batch = toEnhance.slice(0, 20);
131
+ const symbolList = batch.map(s => `- ${s.name} (${s.kind})${s.signature ? `: ${s.signature}` : ""}`).join("\n");
132
+ const prompt = `You are a technical writer generating concise JSDoc-style descriptions for API symbols.
133
+
134
+ File: ${docFile.file}
135
+ Language: ${docFile.language}
136
+
137
+ For each symbol below, write a single short sentence (max 20 words) describing what it does.
138
+ Respond ONLY as JSON: {"symbolName": "description", ...}
139
+
140
+ Symbols:
141
+ ${symbolList}`;
142
+ try {
143
+ const raw = await callClaude(prompt, opts);
144
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
145
+ if (jsonMatch) {
146
+ const descriptions = JSON.parse(jsonMatch[0]);
147
+ for (const sym of docFile.symbols) {
148
+ if (descriptions[sym.name])
149
+ sym.description = descriptions[sym.name];
150
+ }
151
+ }
152
+ }
153
+ catch { /* skip on error */ }
154
+ }
155
+ return enhanced;
156
+ }
@@ -0,0 +1,136 @@
1
+ import https from "node:https";
2
+ // ─── Tokenize ─────────────────────────────────────────────────────────────────
3
+ function tokenize(text) {
4
+ return text
5
+ .replace(/([A-Z])/g, " $1")
6
+ .replace(/[_\-().:<>{}[\],]/g, " ")
7
+ .toLowerCase()
8
+ .split(/\s+/)
9
+ .filter(t => t.length > 1);
10
+ }
11
+ // ─── TF-IDF vectors ───────────────────────────────────────────────────────────
12
+ export function buildTfIdfVectors(skeletons) {
13
+ const docs = [];
14
+ for (const skel of skeletons) {
15
+ for (const sym of skel.symbols) {
16
+ const text = [sym.name, sym.signature ?? "", sym.kind].join(" ");
17
+ docs.push({ file: skel.file, symbol: sym.name, kind: sym.kind, tokens: tokenize(text) });
18
+ }
19
+ }
20
+ if (docs.length === 0)
21
+ return [];
22
+ const df = new Map();
23
+ for (const doc of docs) {
24
+ for (const term of new Set(doc.tokens)) {
25
+ df.set(term, (df.get(term) ?? 0) + 1);
26
+ }
27
+ }
28
+ const N = docs.length;
29
+ const vectors = [];
30
+ for (const doc of docs) {
31
+ const tf = new Map();
32
+ for (const term of doc.tokens)
33
+ tf.set(term, (tf.get(term) ?? 0) + 1);
34
+ const terms = {};
35
+ for (const [term, count] of tf.entries()) {
36
+ const idf = Math.log(N / (df.get(term) ?? 1));
37
+ terms[term] = (count / doc.tokens.length) * idf;
38
+ }
39
+ const norm = Math.sqrt(Object.values(terms).reduce((s, v) => s + v * v, 0));
40
+ vectors.push({ file: doc.file, symbol: doc.symbol, kind: doc.kind, terms, norm });
41
+ }
42
+ return vectors;
43
+ }
44
+ // ─── Cosine search ────────────────────────────────────────────────────────────
45
+ export function cosineSearch(vectors, query, limit = 20) {
46
+ const qTokens = tokenize(query);
47
+ if (qTokens.length === 0)
48
+ return [];
49
+ const qTf = new Map();
50
+ for (const t of qTokens)
51
+ qTf.set(t, (qTf.get(t) ?? 0) + 1);
52
+ const qNorm = Math.sqrt([...qTf.values()].reduce((s, v) => s + (v / qTokens.length) ** 2, 0));
53
+ if (qNorm === 0)
54
+ return [];
55
+ const scores = [];
56
+ for (const vec of vectors) {
57
+ if (vec.norm === 0)
58
+ continue;
59
+ let dot = 0;
60
+ for (const [term, qCount] of qTf.entries()) {
61
+ const qTfidf = qCount / qTokens.length;
62
+ const dTfidf = vec.terms[term] ?? 0;
63
+ dot += qTfidf * dTfidf;
64
+ }
65
+ const score = dot / (qNorm * vec.norm);
66
+ if (score > 0)
67
+ scores.push({ file: vec.file, symbol: vec.symbol, kind: vec.kind, score });
68
+ }
69
+ return scores.sort((a, b) => b.score - a.score).slice(0, limit);
70
+ }
71
+ // ─── Claude re-ranking ────────────────────────────────────────────────────────
72
+ export async function rerankWithClaude(matches, query, opts = {}) {
73
+ if (matches.length === 0)
74
+ return matches;
75
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
76
+ if (!apiKey)
77
+ return matches;
78
+ const list = matches.map((m, i) => `${i + 1}. ${m.symbol} (${m.kind}) in ${m.file}`).join("\n");
79
+ const prompt = `You are helping re-rank code search results for the query: "${query}"
80
+
81
+ Results:
82
+ ${list}
83
+
84
+ Re-rank these by relevance to the query. Respond ONLY with a JSON array of 1-based indices in order of relevance, e.g. [3, 1, 5, 2, 4].`;
85
+ const body = JSON.stringify({
86
+ model: opts.model ?? "claude-sonnet-4-6",
87
+ max_tokens: 256,
88
+ messages: [{ role: "user", content: prompt }],
89
+ });
90
+ try {
91
+ const raw = await new Promise((resolve, reject) => {
92
+ const req = https.request({
93
+ hostname: "api.anthropic.com",
94
+ path: "/v1/messages",
95
+ method: "POST",
96
+ headers: {
97
+ "content-type": "application/json",
98
+ "x-api-key": apiKey,
99
+ "anthropic-version": "2023-06-01",
100
+ "content-length": Buffer.byteLength(body),
101
+ },
102
+ }, (res) => {
103
+ const chunks = [];
104
+ res.on("data", (c) => chunks.push(c));
105
+ res.on("end", () => {
106
+ const text = Buffer.concat(chunks).toString("utf8");
107
+ try {
108
+ const parsed = JSON.parse(text);
109
+ resolve(parsed.content?.[0]?.text ?? "");
110
+ }
111
+ catch {
112
+ resolve("");
113
+ }
114
+ });
115
+ });
116
+ req.on("error", reject);
117
+ req.write(body);
118
+ req.end();
119
+ });
120
+ const arrMatch = raw.match(/\[[\d,\s]+\]/);
121
+ if (arrMatch) {
122
+ const indices = JSON.parse(arrMatch[0]);
123
+ const reranked = indices
124
+ .filter(i => i >= 1 && i <= matches.length)
125
+ .map(i => matches[i - 1]);
126
+ const covered = new Set(indices);
127
+ for (let i = 1; i <= matches.length; i++) {
128
+ if (!covered.has(i))
129
+ reranked.push(matches[i - 1]);
130
+ }
131
+ return reranked;
132
+ }
133
+ }
134
+ catch { /* fall through */ }
135
+ return matches;
136
+ }