codebase-wiki 0.1.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/CHANGELOG.md +34 -0
- package/LICENSE +17 -0
- package/README.md +130 -0
- package/dist/CHANGELOG.md +34 -0
- package/dist/LICENSE +17 -0
- package/dist/README.md +130 -0
- package/dist/chunks/build.mjs +1976 -0
- package/dist/chunks/config.mjs +174 -0
- package/dist/chunks/llm.mjs +214 -0
- package/dist/code-wiki-mcp.mjs +367 -0
- package/dist/code-wiki.mjs +11 -0
- package/dist/codewiki.mjs +2002 -0
- package/dist/grammars/c.wasm +0 -0
- package/dist/grammars/cpp.wasm +0 -0
- package/dist/grammars/go.wasm +0 -0
- package/dist/grammars/java.wasm +0 -0
- package/dist/grammars/javascript.wasm +0 -0
- package/dist/grammars/python.wasm +0 -0
- package/dist/grammars/ruby.wasm +0 -0
- package/dist/grammars/rust.wasm +0 -0
- package/dist/grammars/tsx.wasm +0 -0
- package/dist/grammars/typescript.wasm +0 -0
- package/dist/plugin/.mcp.json +11 -0
- package/dist/plugin/CLAUDE.md +28 -0
- package/dist/plugin/commands/wiki-architecture.md +10 -0
- package/dist/plugin/commands/wiki-build.md +23 -0
- package/dist/plugin/commands/wiki-enrich.md +22 -0
- package/dist/plugin/commands/wiki-graph.md +16 -0
- package/dist/plugin/commands/wiki-help.md +49 -0
- package/dist/plugin/commands/wiki-refresh.md +13 -0
- package/dist/plugin/commands/wiki-search.md +18 -0
- package/dist/plugin/commands/wiki-symbol.md +21 -0
- package/dist/plugin/hooks/hooks.json +74 -0
- package/dist/plugin/hooks/nudge.mjs +197 -0
- package/dist/plugin/hooks/post-read-reminder.mjs +56 -0
- package/dist/plugin/hooks/post-tool-use.mjs +60 -0
- package/dist/plugin/hooks/pre-compact.mjs +25 -0
- package/dist/plugin/hooks/session-start.mjs +38 -0
- package/dist/plugin/hooks/user-prompt-submit.mjs +28 -0
- package/dist/plugin/manifest.json +54 -0
- package/package.json +100 -0
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
import path__default from 'node:path';
|
|
2
|
+
import { promises } from 'node:fs';
|
|
3
|
+
import { c as repoRel, a as log, t as toPosix, l as loadConfig, d as child, f as configSummary, w as writeJson, b as atomicWriteText, e as exists } from './config.mjs';
|
|
4
|
+
import ignore from 'ignore';
|
|
5
|
+
import { globby } from 'globby';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { encode } from 'gpt-tokenizer/encoding/cl100k_base';
|
|
8
|
+
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
9
|
+
|
|
10
|
+
const EXT_TO_LANG = {
|
|
11
|
+
".ts": "typescript",
|
|
12
|
+
".mts": "typescript",
|
|
13
|
+
".cts": "typescript",
|
|
14
|
+
".tsx": "tsx",
|
|
15
|
+
".js": "javascript",
|
|
16
|
+
".mjs": "javascript",
|
|
17
|
+
".cjs": "javascript",
|
|
18
|
+
".jsx": "jsx",
|
|
19
|
+
".py": "python",
|
|
20
|
+
".pyi": "python",
|
|
21
|
+
".go": "go",
|
|
22
|
+
".rs": "rust",
|
|
23
|
+
".java": "java",
|
|
24
|
+
".c": "c",
|
|
25
|
+
".h": "c",
|
|
26
|
+
".cpp": "cpp",
|
|
27
|
+
".cc": "cpp",
|
|
28
|
+
".cxx": "cpp",
|
|
29
|
+
".hpp": "cpp",
|
|
30
|
+
".hh": "cpp",
|
|
31
|
+
".rb": "ruby"
|
|
32
|
+
};
|
|
33
|
+
function detectLanguage(filePath) {
|
|
34
|
+
const ext = path__default.extname(filePath).toLowerCase();
|
|
35
|
+
return EXT_TO_LANG[ext] ?? "unknown";
|
|
36
|
+
}
|
|
37
|
+
async function walkFiles(opts) {
|
|
38
|
+
const { root, ignoreGlobs, includeExtensions = [], maxFiles = 0 } = opts;
|
|
39
|
+
const allIgnores = [".git", ".codewiki", ".codewiki.new", ...ignoreGlobs];
|
|
40
|
+
const ig = ignore({ allowRelativePaths: true }).add(allIgnores);
|
|
41
|
+
const gitignore = path__default.join(root, ".gitignore");
|
|
42
|
+
try {
|
|
43
|
+
const content = await promises.readFile(gitignore, "utf8");
|
|
44
|
+
ig.add(content);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
const paths = await globby("**", {
|
|
48
|
+
cwd: root,
|
|
49
|
+
absolute: true,
|
|
50
|
+
onlyFiles: true,
|
|
51
|
+
dot: false,
|
|
52
|
+
gitignore: false,
|
|
53
|
+
// we apply gitignore ourselves for portability
|
|
54
|
+
ignore: allIgnores
|
|
55
|
+
});
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const abs of paths) {
|
|
58
|
+
if (maxFiles > 0 && results.length >= maxFiles) break;
|
|
59
|
+
const rel = path__default.relative(root, abs).replace(/\\/g, "/");
|
|
60
|
+
if (ig.ignores(rel)) continue;
|
|
61
|
+
let stat;
|
|
62
|
+
try {
|
|
63
|
+
stat = await promises.stat(abs);
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!stat.isFile()) continue;
|
|
68
|
+
results.push({
|
|
69
|
+
absPath: abs,
|
|
70
|
+
repoPath: repoRel(abs, root),
|
|
71
|
+
size: stat.size,
|
|
72
|
+
mtimeMs: stat.mtimeMs,
|
|
73
|
+
language: detectLanguage(abs)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
results.sort((a, b) => a.repoPath < b.repoPath ? -1 : 1);
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const walker = {
|
|
81
|
+
__proto__: null,
|
|
82
|
+
detectLanguage: detectLanguage,
|
|
83
|
+
walkFiles: walkFiles
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const TEST_PATTERNS = [
|
|
87
|
+
/(^|\/)__tests__\//,
|
|
88
|
+
/\.test\.[a-z]+$/,
|
|
89
|
+
/\.spec\.[a-z]+$/,
|
|
90
|
+
/(^|\/)test_(.+)\.[a-z]+$/,
|
|
91
|
+
/(^|\/)(.+)_test\.[a-z]+$/,
|
|
92
|
+
/(^|\/)spec\//
|
|
93
|
+
];
|
|
94
|
+
function isTestFile(repoPath) {
|
|
95
|
+
return TEST_PATTERNS.some((re) => re.test(repoPath));
|
|
96
|
+
}
|
|
97
|
+
function moduleForFile(repoPath, cfg) {
|
|
98
|
+
if (isTestFile(repoPath)) return "_tests";
|
|
99
|
+
for (const [from, to] of Object.entries(cfg.modules.alias)) {
|
|
100
|
+
if (repoPath === from || repoPath.startsWith(`${from}/`)) {
|
|
101
|
+
const remainder = repoPath.slice(from.length).replace(/^\//, "");
|
|
102
|
+
const top2 = remainder.split("/")[0] || "_root";
|
|
103
|
+
const candidate = `${to}/${remainder === "" ? "" : top2}`.replace(/\/$/, "");
|
|
104
|
+
return candidate.split("/")[0] || "_root";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const top = repoPath.split("/")[0];
|
|
108
|
+
return top || "_root";
|
|
109
|
+
}
|
|
110
|
+
function groupModules(files, cfg) {
|
|
111
|
+
const map = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const f of files) {
|
|
113
|
+
if (f.repoPath.startsWith(".codewiki/")) continue;
|
|
114
|
+
const id = moduleForFile(f.repoPath, cfg);
|
|
115
|
+
let entry = map.get(id);
|
|
116
|
+
if (!entry) {
|
|
117
|
+
entry = {
|
|
118
|
+
id,
|
|
119
|
+
title: id,
|
|
120
|
+
files: [],
|
|
121
|
+
publicSurface: []
|
|
122
|
+
};
|
|
123
|
+
map.set(id, entry);
|
|
124
|
+
}
|
|
125
|
+
entry.files.push(f.repoPath);
|
|
126
|
+
for (const s of f.publicSymbols) {
|
|
127
|
+
entry.publicSurface.push(`${s.kind === "method" ? "method" : s.kind} ${s.name}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const id of [...map.keys()]) {
|
|
131
|
+
if (cfg.modules.ignore.includes(id)) {
|
|
132
|
+
map.delete(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sha256Hex(input) {
|
|
139
|
+
return createHash("sha256").update(input).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function countTokens(input) {
|
|
143
|
+
if (!input) return 0;
|
|
144
|
+
return encode(input).length;
|
|
145
|
+
}
|
|
146
|
+
function truncateToTokens(text, budget, ellipsis = "\u2026") {
|
|
147
|
+
const current = countTokens(text);
|
|
148
|
+
if (current <= budget) return text;
|
|
149
|
+
let lo = 0;
|
|
150
|
+
let hi = text.length;
|
|
151
|
+
while (lo < hi) {
|
|
152
|
+
const mid = lo + hi + 1 >> 1;
|
|
153
|
+
if (countTokens(text.slice(0, mid)) <= budget - 1) {
|
|
154
|
+
lo = mid;
|
|
155
|
+
} else {
|
|
156
|
+
hi = mid - 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return text.slice(0, lo).trimEnd() + (lo > 0 ? " " + ellipsis : "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const GENERATOR = `code-wiki`;
|
|
163
|
+
const VERSION = "0.1.0";
|
|
164
|
+
function frontmatter(page) {
|
|
165
|
+
const srcBlock = page.source ? `
|
|
166
|
+
source:
|
|
167
|
+
repoPath: ${yamlStr(page.source.repoPath)}
|
|
168
|
+
language: ${yamlStr(page.source.language ?? "null")}
|
|
169
|
+
module: ${yamlStr(page.source.module ?? "null")}${page.source.lineRange ? `
|
|
170
|
+
lineRange: [${page.source.lineRange[0]}, ${page.source.lineRange[1]}]` : ""}${page.source.symbol ? `
|
|
171
|
+
symbol: ${yamlStr(page.source.symbol)}` : ""}${page.source.symbolKind ? `
|
|
172
|
+
symbolKind: ${yamlStr(page.source.symbolKind)}` : ""}${page.source.signature ? `
|
|
173
|
+
signature: ${yamlStr(page.source.signature)}` : ""}` : "";
|
|
174
|
+
return [
|
|
175
|
+
"---",
|
|
176
|
+
`kind: ${page.kind}`,
|
|
177
|
+
`id: ${yamlStr(page.id)}`,
|
|
178
|
+
`title: ${yamlStr(page.title)}`,
|
|
179
|
+
`path: ${yamlStr(page.path)}`,
|
|
180
|
+
`tokens: ${page.tokens}`,
|
|
181
|
+
`stale: ${page.stale}`,
|
|
182
|
+
`generatedAt: ${yamlStr(page.generatedAt)}`,
|
|
183
|
+
`generator: ${yamlStr(page.generator)}` + srcBlock,
|
|
184
|
+
"---",
|
|
185
|
+
""
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
188
|
+
function yamlStr(s) {
|
|
189
|
+
if (s == null) return "null";
|
|
190
|
+
const escaped = s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
191
|
+
return `"${escaped}"`;
|
|
192
|
+
}
|
|
193
|
+
function pageStats(body) {
|
|
194
|
+
return countTokens(body);
|
|
195
|
+
}
|
|
196
|
+
function makePage(kind, id, title, relPath, data, source = void 0, stale = false) {
|
|
197
|
+
return {
|
|
198
|
+
kind,
|
|
199
|
+
id,
|
|
200
|
+
title,
|
|
201
|
+
path: relPath,
|
|
202
|
+
source,
|
|
203
|
+
tokens: 0,
|
|
204
|
+
// filled in after rendering
|
|
205
|
+
stale,
|
|
206
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
207
|
+
generator: `${GENERATOR}/${VERSION}`,
|
|
208
|
+
data
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildArchitecturePage(meta, files, cfg, importEdges = [], dependents = {}) {
|
|
213
|
+
const modules = groupModules(files, cfg);
|
|
214
|
+
const lines = [];
|
|
215
|
+
lines.push("# Architecture");
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push(
|
|
218
|
+
`> ${meta.fileCount} files, ${meta.moduleCount} modules, ${meta.symbolCount} symbols.`
|
|
219
|
+
);
|
|
220
|
+
lines.push("");
|
|
221
|
+
const visible = [...modules.entries()].filter(([id]) => !cfg.modules.ignore.includes(id)).sort(([a], [b]) => a < b ? -1 : 1);
|
|
222
|
+
if (visible.length > 0) {
|
|
223
|
+
const moduleEdges = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const e of importEdges) {
|
|
225
|
+
const fromMod = moduleForFile(e.from, cfg);
|
|
226
|
+
const toMod = moduleForFile(e.to, cfg);
|
|
227
|
+
if (fromMod === toMod) continue;
|
|
228
|
+
if (!visible.find(([id]) => id === fromMod)) continue;
|
|
229
|
+
if (!visible.find(([id]) => id === toMod)) continue;
|
|
230
|
+
const key = `${fromMod}|${toMod}`;
|
|
231
|
+
moduleEdges.set(key, (moduleEdges.get(key) ?? 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
lines.push("## Module map");
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push("```mermaid");
|
|
236
|
+
lines.push("graph LR");
|
|
237
|
+
const cap = cfg.budgets.mermaidNodes;
|
|
238
|
+
const displayedNodes = visible.slice(0, cap);
|
|
239
|
+
for (const [id, m] of displayedNodes) {
|
|
240
|
+
lines.push(` ${diagramId(id)}["${m.title}"]`);
|
|
241
|
+
}
|
|
242
|
+
if (visible.length > cap) {
|
|
243
|
+
lines.push(` more["...(${visible.length - cap} more)"]`);
|
|
244
|
+
}
|
|
245
|
+
const edgesArr = [...moduleEdges.entries()].filter(
|
|
246
|
+
([k]) => displayedNodes.find(([id]) => id === k.split("|")[0]) && displayedNodes.find(([id]) => id === k.split("|")[1])
|
|
247
|
+
);
|
|
248
|
+
for (const [key, count] of edgesArr.slice(0, cap)) {
|
|
249
|
+
const [from, to] = key.split("|");
|
|
250
|
+
lines.push(` ${diagramId(from)} -->|${count} edge${count === 1 ? "" : "es"}| ${diagramId(to)}`);
|
|
251
|
+
}
|
|
252
|
+
lines.push("```");
|
|
253
|
+
lines.push("");
|
|
254
|
+
}
|
|
255
|
+
lines.push("## Modules");
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(
|
|
258
|
+
"| Module | Files | Public surface | Imports | Imported by |"
|
|
259
|
+
);
|
|
260
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
261
|
+
for (const [id, m] of visible) {
|
|
262
|
+
const purpose = m.publicSurface.length > 0 ? `\`${m.publicSurface.slice(0, 3).join("`, `")}\`...` : "_Internal-only_";
|
|
263
|
+
const outCount = countOutgoing(importEdges, files, cfg, id);
|
|
264
|
+
const inCount = countIncoming(dependents, files, cfg, id);
|
|
265
|
+
lines.push(
|
|
266
|
+
`| [${m.title}](./modules/${id}.md) | ${m.files.length} | ${purpose} | ${outCount} | ${inCount} |`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
lines.push("");
|
|
270
|
+
const body = lines.join("\n");
|
|
271
|
+
const tokens = pageStats(body);
|
|
272
|
+
const data = {
|
|
273
|
+
meta,
|
|
274
|
+
modules: visible.map(([id, m]) => ({
|
|
275
|
+
id,
|
|
276
|
+
title: m.title,
|
|
277
|
+
fileCount: m.files.length,
|
|
278
|
+
purpose: m.publicSurface.length > 0 ? `Public: ${m.publicSurface.slice(0, 3).join(", ")}...` : "Internal-only",
|
|
279
|
+
pagePath: `modules/${id}.md`,
|
|
280
|
+
importsCount: countOutgoing(importEdges, files, cfg, id),
|
|
281
|
+
dependentsCount: countIncoming(dependents, files, cfg, id)
|
|
282
|
+
}))
|
|
283
|
+
};
|
|
284
|
+
const page = makePage(
|
|
285
|
+
"architecture",
|
|
286
|
+
"architecture",
|
|
287
|
+
"Architecture",
|
|
288
|
+
"architecture.md",
|
|
289
|
+
data,
|
|
290
|
+
void 0,
|
|
291
|
+
false
|
|
292
|
+
);
|
|
293
|
+
page.tokens = tokens;
|
|
294
|
+
return { page, body, tokens };
|
|
295
|
+
}
|
|
296
|
+
function renderArchitecture(page, body) {
|
|
297
|
+
return frontmatter(page) + body;
|
|
298
|
+
}
|
|
299
|
+
function diagramId(s) {
|
|
300
|
+
return s.replace(/[^A-Za-z0-9]/g, "_").replace(/^(\d)/, "_$1");
|
|
301
|
+
}
|
|
302
|
+
function countOutgoing(edges, _files, cfg, moduleId) {
|
|
303
|
+
let n = 0;
|
|
304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
305
|
+
for (const e of edges) {
|
|
306
|
+
const fromMod = moduleForFile(e.from, cfg);
|
|
307
|
+
if (fromMod !== moduleId) continue;
|
|
308
|
+
const toMod = moduleForFile(e.to, cfg);
|
|
309
|
+
const key = `${fromMod}|${toMod}`;
|
|
310
|
+
if (!seen.has(key)) {
|
|
311
|
+
seen.add(key);
|
|
312
|
+
n++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return n;
|
|
316
|
+
}
|
|
317
|
+
function countIncoming(dependents, _files, cfg, moduleId) {
|
|
318
|
+
const counts = /* @__PURE__ */ new Set();
|
|
319
|
+
for (const t of Object.keys(dependents)) {
|
|
320
|
+
const tgt = moduleForFile(t, cfg);
|
|
321
|
+
if (tgt === moduleId) {
|
|
322
|
+
for (const d of dependents[t] ?? []) counts.add(moduleForFile(d, cfg));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return counts.size;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildIndexPage(meta, modules, modulePagePaths, moduleTokens, recent, cfg) {
|
|
329
|
+
const modList = [...modules.entries()].filter(([id]) => !cfg.modules.ignore.includes(id)).map(([id, m]) => ({
|
|
330
|
+
...m,
|
|
331
|
+
pagePath: modulePagePaths.get(id) ?? `modules/${id}.md`,
|
|
332
|
+
tokens: moduleTokens.get(id) ?? 0
|
|
333
|
+
})).sort((a, b) => a.id < b.id ? -1 : 1);
|
|
334
|
+
const data = {
|
|
335
|
+
meta,
|
|
336
|
+
modules: modList,
|
|
337
|
+
recentlyChanged: recent
|
|
338
|
+
};
|
|
339
|
+
const lines = [];
|
|
340
|
+
lines.push("# Code Index");
|
|
341
|
+
lines.push("");
|
|
342
|
+
lines.push(
|
|
343
|
+
`> ${meta.fileCount} files across ${meta.moduleCount} modules. Generated ${meta.updatedAt}.`
|
|
344
|
+
);
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push("## Modules");
|
|
347
|
+
lines.push("");
|
|
348
|
+
for (const m of modList) {
|
|
349
|
+
lines.push(`- [${m.title}](./${m.pagePath}) \u2014 ${m.files.length} files, ~${m.tokens} tokens`);
|
|
350
|
+
}
|
|
351
|
+
if (recent.length > 0) {
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push("## Recently changed");
|
|
354
|
+
lines.push("");
|
|
355
|
+
for (const p of recent) lines.push(`- \`${p}\``);
|
|
356
|
+
}
|
|
357
|
+
lines.push("");
|
|
358
|
+
lines.push("## Search hints");
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push("- Use `/wiki-search <query>` or the `wiki_search` MCP tool.");
|
|
361
|
+
lines.push("- Use `/wiki-symbol <path>:<name>` to drill into one symbol.");
|
|
362
|
+
lines.push("- Use `/wiki-architecture` for the system view (and a Mermaid diagram).");
|
|
363
|
+
lines.push("");
|
|
364
|
+
const body = lines.join("\n");
|
|
365
|
+
const tokens = pageStats(body);
|
|
366
|
+
const page = makePage(
|
|
367
|
+
"index",
|
|
368
|
+
"index",
|
|
369
|
+
"Code Index",
|
|
370
|
+
"INDEX.md",
|
|
371
|
+
data,
|
|
372
|
+
void 0,
|
|
373
|
+
false
|
|
374
|
+
);
|
|
375
|
+
page.tokens = tokens;
|
|
376
|
+
return { page, body, tokens };
|
|
377
|
+
}
|
|
378
|
+
function renderIndex(page, body) {
|
|
379
|
+
return frontmatter(page) + body;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildModulePage(module, files, filePagePaths, cfg, importEdges = [], dependents = {}) {
|
|
383
|
+
const lines = [];
|
|
384
|
+
lines.push(`# Module: ${module.title}`);
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push(
|
|
387
|
+
`> ${module.files.length} files, ${module.publicSurface.length} public symbols.`
|
|
388
|
+
);
|
|
389
|
+
lines.push("");
|
|
390
|
+
const outgoing = /* @__PURE__ */ new Set();
|
|
391
|
+
for (const e of importEdges) {
|
|
392
|
+
const fromMod = moduleForFile(e.from, cfg);
|
|
393
|
+
if (fromMod === module.id) {
|
|
394
|
+
outgoing.add(moduleForFile(e.to, cfg));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const incoming = /* @__PURE__ */ new Set();
|
|
398
|
+
for (const t of Object.keys(dependents)) {
|
|
399
|
+
const tgtMod = moduleForFile(t, cfg);
|
|
400
|
+
if (tgtMod === module.id) {
|
|
401
|
+
for (const d of dependents[t] ?? []) incoming.add(moduleForFile(d, cfg));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (outgoing.size > 0 || incoming.size > 0) {
|
|
405
|
+
lines.push("## Dependencies");
|
|
406
|
+
lines.push("");
|
|
407
|
+
lines.push("```mermaid");
|
|
408
|
+
lines.push("graph LR");
|
|
409
|
+
const selfId = "this";
|
|
410
|
+
lines.push(` ${selfId}["${module.title}"]`);
|
|
411
|
+
let i = 0;
|
|
412
|
+
for (const m of outgoing) {
|
|
413
|
+
if (m === module.id) continue;
|
|
414
|
+
lines.push(` out${i}["${m}"]`);
|
|
415
|
+
lines.push(` ${selfId} --> out${i}`);
|
|
416
|
+
i++;
|
|
417
|
+
}
|
|
418
|
+
let j = 0;
|
|
419
|
+
for (const m of incoming) {
|
|
420
|
+
if (m === module.id) continue;
|
|
421
|
+
lines.push(` in${j}["${m}"]`);
|
|
422
|
+
lines.push(` in${j} --> ${selfId}`);
|
|
423
|
+
j++;
|
|
424
|
+
}
|
|
425
|
+
lines.push("```");
|
|
426
|
+
lines.push("");
|
|
427
|
+
}
|
|
428
|
+
if (module.publicSurface.length > 0) {
|
|
429
|
+
lines.push("## Public surface");
|
|
430
|
+
lines.push("");
|
|
431
|
+
for (const s of module.publicSurface.slice(0, 20)) {
|
|
432
|
+
lines.push(`- \`${s}\``);
|
|
433
|
+
}
|
|
434
|
+
if (module.publicSurface.length > 20) {
|
|
435
|
+
lines.push(`- _(${module.publicSurface.length - 20} more \u2014 see \`/wiki-search <id>\` for the rest)_`);
|
|
436
|
+
}
|
|
437
|
+
lines.push("");
|
|
438
|
+
}
|
|
439
|
+
lines.push("## Files");
|
|
440
|
+
lines.push("");
|
|
441
|
+
lines.push("| File | Summary | Symbols |");
|
|
442
|
+
lines.push("| --- | --- | --- |");
|
|
443
|
+
for (const fp of module.files) {
|
|
444
|
+
const f = files.find((x) => x.repoPath === fp);
|
|
445
|
+
if (!f) continue;
|
|
446
|
+
const pagePath2 = filePagePaths.get(fp) ?? `files/${fp}.md`;
|
|
447
|
+
const summary = (f.oneLineSummary || "").replace(/\|/g, "\\|").slice(0, 120);
|
|
448
|
+
lines.push(`| [${fp}](./${pagePath2}) | ${summary} | ${f.symbols.length} |`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("");
|
|
451
|
+
const body = lines.join("\n");
|
|
452
|
+
const tokens = pageStats(body);
|
|
453
|
+
const data = {
|
|
454
|
+
module,
|
|
455
|
+
files: module.files.map((fp) => {
|
|
456
|
+
const f = files.find((x) => x.repoPath === fp);
|
|
457
|
+
if (!f) return null;
|
|
458
|
+
return {
|
|
459
|
+
repoPath: fp,
|
|
460
|
+
oneLineSummary: f.oneLineSummary,
|
|
461
|
+
pagePath: filePagePaths.get(fp) ?? `files/${fp}.md`,
|
|
462
|
+
symbols: f.symbols.length
|
|
463
|
+
};
|
|
464
|
+
}).filter((x) => x !== null),
|
|
465
|
+
importsCount: outgoing.size,
|
|
466
|
+
dependentsCount: incoming.size
|
|
467
|
+
};
|
|
468
|
+
const pagePath = `modules/${module.id}.md`;
|
|
469
|
+
const page = makePage(
|
|
470
|
+
"module",
|
|
471
|
+
module.id,
|
|
472
|
+
module.title,
|
|
473
|
+
pagePath,
|
|
474
|
+
data,
|
|
475
|
+
{
|
|
476
|
+
repoPath: "",
|
|
477
|
+
language: null,
|
|
478
|
+
module: module.id
|
|
479
|
+
},
|
|
480
|
+
false
|
|
481
|
+
);
|
|
482
|
+
page.tokens = tokens;
|
|
483
|
+
return { page, body, tokens };
|
|
484
|
+
}
|
|
485
|
+
function renderModule(page, body) {
|
|
486
|
+
return frontmatter(page) + body;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function buildFilePage(file) {
|
|
490
|
+
const lines = [];
|
|
491
|
+
lines.push(`# File: ${file.repoPath}`);
|
|
492
|
+
lines.push("");
|
|
493
|
+
if (file.oneLineSummary) lines.push(`> ${file.oneLineSummary}`);
|
|
494
|
+
lines.push("");
|
|
495
|
+
lines.push(`- **Language**: ${file.language ?? "unknown"}`);
|
|
496
|
+
lines.push(`- **Size**: ${file.size} bytes`);
|
|
497
|
+
lines.push(`- **Symbols**: ${file.symbols.length}`);
|
|
498
|
+
lines.push("");
|
|
499
|
+
if (file.symbols.length > 0) {
|
|
500
|
+
lines.push("## Symbols");
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push("| Name | Kind | Lines | Signature |");
|
|
503
|
+
lines.push("| --- | --- | --- | --- |");
|
|
504
|
+
for (const s of file.symbols) {
|
|
505
|
+
const sig = (s.signature || "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
506
|
+
lines.push(
|
|
507
|
+
`| ${s.name} | ${s.kind} | ${s.lineRange[0]}-${s.lineRange[1]} | \`${sig.slice(0, 200)}\` |`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
lines.push("");
|
|
511
|
+
} else {
|
|
512
|
+
lines.push("_No symbols extracted (language not yet supported or file is empty)._");
|
|
513
|
+
lines.push("");
|
|
514
|
+
}
|
|
515
|
+
lines.push("## Notes");
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push(
|
|
518
|
+
"This is the static summary. Run `/wiki-enrich` (with `ANTHROPIC_API_KEY` set) for richer prose."
|
|
519
|
+
);
|
|
520
|
+
lines.push("");
|
|
521
|
+
const body = lines.join("\n");
|
|
522
|
+
const tokens = pageStats(body);
|
|
523
|
+
const pagePath = `files/${file.repoPath}.md`;
|
|
524
|
+
const data = { file };
|
|
525
|
+
const page = makePage(
|
|
526
|
+
"file",
|
|
527
|
+
file.repoPath,
|
|
528
|
+
file.repoPath,
|
|
529
|
+
pagePath,
|
|
530
|
+
data,
|
|
531
|
+
{
|
|
532
|
+
repoPath: file.repoPath,
|
|
533
|
+
language: file.language,
|
|
534
|
+
module: null
|
|
535
|
+
},
|
|
536
|
+
false
|
|
537
|
+
);
|
|
538
|
+
page.tokens = tokens;
|
|
539
|
+
return { page, body, tokens };
|
|
540
|
+
}
|
|
541
|
+
function renderFile(page, body) {
|
|
542
|
+
return frontmatter(page) + body;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const SCHEMA_VERSION = 1;
|
|
546
|
+
|
|
547
|
+
const GRAMMAR_PATHS = {
|
|
548
|
+
typescript: {
|
|
549
|
+
basename: "tree-sitter-typescript.wasm",
|
|
550
|
+
candidateNames: [
|
|
551
|
+
"dist/grammars/typescript.wasm",
|
|
552
|
+
"node_modules/tree-sitter-typescript/tree-sitter-typescript.wasm",
|
|
553
|
+
// Bundled plugin: chunks live at dist/chunks/*.mjs; grammars live at
|
|
554
|
+
// dist/grammars/*.wasm. We compute the chunk's dirname lazily.
|
|
555
|
+
`__import_meta_dirname__/../grammars/typescript.wasm`
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
tsx: {
|
|
559
|
+
basename: "tree-sitter-tsx.wasm",
|
|
560
|
+
candidateNames: [
|
|
561
|
+
"dist/grammars/tsx.wasm",
|
|
562
|
+
"node_modules/tree-sitter-typescript/tree-sitter-tsx.wasm",
|
|
563
|
+
`__import_meta_dirname__/../grammars/tsx.wasm`
|
|
564
|
+
]
|
|
565
|
+
},
|
|
566
|
+
javascript: {
|
|
567
|
+
basename: "tree-sitter-javascript.wasm",
|
|
568
|
+
candidateNames: [
|
|
569
|
+
"dist/grammars/javascript.wasm",
|
|
570
|
+
"node_modules/tree-sitter-javascript/tree-sitter-javascript.wasm",
|
|
571
|
+
`__import_meta_dirname__/../grammars/javascript.wasm`
|
|
572
|
+
]
|
|
573
|
+
},
|
|
574
|
+
python: {
|
|
575
|
+
basename: "tree-sitter-python.wasm",
|
|
576
|
+
candidateNames: [
|
|
577
|
+
"dist/grammars/python.wasm",
|
|
578
|
+
"node_modules/tree-sitter-python/tree-sitter-python.wasm",
|
|
579
|
+
`__import_meta_dirname__/../grammars/python.wasm`
|
|
580
|
+
]
|
|
581
|
+
},
|
|
582
|
+
go: {
|
|
583
|
+
basename: "tree-sitter-go.wasm",
|
|
584
|
+
candidateNames: [
|
|
585
|
+
"dist/grammars/go.wasm",
|
|
586
|
+
"node_modules/tree-sitter-go/tree-sitter-go.wasm",
|
|
587
|
+
`__import_meta_dirname__/../grammars/go.wasm`
|
|
588
|
+
]
|
|
589
|
+
},
|
|
590
|
+
rust: {
|
|
591
|
+
basename: "tree-sitter-rust.wasm",
|
|
592
|
+
candidateNames: [
|
|
593
|
+
"dist/grammars/rust.wasm",
|
|
594
|
+
"node_modules/tree-sitter-rust/tree-sitter-rust.wasm",
|
|
595
|
+
`__import_meta_dirname__/../grammars/rust.wasm`
|
|
596
|
+
]
|
|
597
|
+
},
|
|
598
|
+
java: {
|
|
599
|
+
basename: "tree-sitter-java.wasm",
|
|
600
|
+
candidateNames: [
|
|
601
|
+
"dist/grammars/java.wasm",
|
|
602
|
+
"node_modules/tree-sitter-java/tree-sitter-java.wasm",
|
|
603
|
+
`__import_meta_dirname__/../grammars/java.wasm`
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
c: {
|
|
607
|
+
basename: "tree-sitter-c.wasm",
|
|
608
|
+
candidateNames: [
|
|
609
|
+
"dist/grammars/c.wasm",
|
|
610
|
+
"node_modules/tree-sitter-c/tree-sitter-c.wasm",
|
|
611
|
+
`__import_meta_dirname__/../grammars/c.wasm`
|
|
612
|
+
]
|
|
613
|
+
},
|
|
614
|
+
cpp: {
|
|
615
|
+
basename: "tree-sitter-cpp.wasm",
|
|
616
|
+
candidateNames: [
|
|
617
|
+
"dist/grammars/cpp.wasm",
|
|
618
|
+
"node_modules/tree-sitter-cpp/tree-sitter-cpp.wasm",
|
|
619
|
+
`__import_meta_dirname__/../grammars/cpp.wasm`
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
ruby: {
|
|
623
|
+
basename: "tree-sitter-ruby.wasm",
|
|
624
|
+
candidateNames: [
|
|
625
|
+
"dist/grammars/ruby.wasm",
|
|
626
|
+
"node_modules/tree-sitter-ruby/tree-sitter-ruby.wasm",
|
|
627
|
+
`__import_meta_dirname__/../grammars/ruby.wasm`
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
function expandImportMetaDirname(candidates) {
|
|
632
|
+
if (typeof import.meta.dirname !== "string") return candidates;
|
|
633
|
+
return candidates.map(
|
|
634
|
+
(c) => c.includes("__import_meta_dirname__") ? c.replace(/__import_meta_dirname__/g, import.meta.dirname) : c
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
let initPromise = null;
|
|
638
|
+
const languageCache = /* @__PURE__ */ new Map();
|
|
639
|
+
async function ensureParserInit() {
|
|
640
|
+
if (!initPromise) {
|
|
641
|
+
initPromise = Parser.init().then(() => {
|
|
642
|
+
log.debug("web-tree-sitter Parser initialised");
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return initPromise;
|
|
646
|
+
}
|
|
647
|
+
async function findWasm(candidates) {
|
|
648
|
+
const expanded = expandImportMetaDirname(candidates);
|
|
649
|
+
for (const rel of expanded) {
|
|
650
|
+
if (path__default.isAbsolute(rel)) {
|
|
651
|
+
try {
|
|
652
|
+
await promises.access(rel);
|
|
653
|
+
return rel;
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let cwd = process.cwd();
|
|
659
|
+
for (let i = 0; i < 8; i++) {
|
|
660
|
+
for (const rel of expanded) {
|
|
661
|
+
if (path__default.isAbsolute(rel)) continue;
|
|
662
|
+
const abs = path__default.join(cwd, rel);
|
|
663
|
+
try {
|
|
664
|
+
await promises.access(abs);
|
|
665
|
+
return abs;
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const parent = path__default.dirname(cwd);
|
|
670
|
+
if (parent === cwd) break;
|
|
671
|
+
cwd = parent;
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
async function getLanguage(lang) {
|
|
676
|
+
const cached = languageCache.get(lang);
|
|
677
|
+
if (cached) return cached;
|
|
678
|
+
const spec = GRAMMAR_PATHS[lang];
|
|
679
|
+
if (!spec) return null;
|
|
680
|
+
await ensureParserInit();
|
|
681
|
+
const wasmPath = await findWasm(spec.candidateNames);
|
|
682
|
+
if (!wasmPath) {
|
|
683
|
+
log.warn({ lang }, "tree-sitter grammar wasm not found; check dist/grammars/");
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const bytes = await promises.readFile(wasmPath);
|
|
688
|
+
const language = await Language.load(bytes);
|
|
689
|
+
languageCache.set(lang, language);
|
|
690
|
+
return language;
|
|
691
|
+
} catch (err) {
|
|
692
|
+
log.warn({ lang, err: String(err) }, "failed to load grammar");
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const TYPESCRIPT_QUERY = `
|
|
698
|
+
; top-level function declarations
|
|
699
|
+
(function_declaration
|
|
700
|
+
name: (identifier) @def.function.name) @def.function
|
|
701
|
+
|
|
702
|
+
; exported function declarations
|
|
703
|
+
(export_statement
|
|
704
|
+
(function_declaration
|
|
705
|
+
name: (identifier) @def.function.name) @def.function)
|
|
706
|
+
|
|
707
|
+
; variable-declared functions / arrows
|
|
708
|
+
(variable_declaration
|
|
709
|
+
(variable_declarator
|
|
710
|
+
name: (identifier) @def.function.name
|
|
711
|
+
value: [(function_expression) (arrow_function)]) @def.function)
|
|
712
|
+
|
|
713
|
+
; class declarations
|
|
714
|
+
(class_declaration
|
|
715
|
+
name: (type_identifier) @def.class.name) @def.class
|
|
716
|
+
(export_statement
|
|
717
|
+
(class_declaration
|
|
718
|
+
name: (type_identifier) @def.class.name) @def.class)
|
|
719
|
+
|
|
720
|
+
; class members (methods)
|
|
721
|
+
(method_definition
|
|
722
|
+
name: (property_identifier) @def.method.name) @def.method
|
|
723
|
+
|
|
724
|
+
; interfaces
|
|
725
|
+
(interface_declaration
|
|
726
|
+
name: (type_identifier) @def.interface.name) @def.interface
|
|
727
|
+
(export_statement
|
|
728
|
+
(interface_declaration
|
|
729
|
+
name: (type_identifier) @def.interface.name) @def.interface)
|
|
730
|
+
|
|
731
|
+
; type aliases
|
|
732
|
+
(type_alias_declaration
|
|
733
|
+
name: (type_identifier) @def.type.name) @def.type
|
|
734
|
+
(export_statement
|
|
735
|
+
(type_alias_declaration
|
|
736
|
+
name: (type_identifier) @def.type.name) @def.type)
|
|
737
|
+
|
|
738
|
+
; enums
|
|
739
|
+
(enum_declaration
|
|
740
|
+
name: (identifier) @def.enum.name) @def.enum
|
|
741
|
+
(export_statement
|
|
742
|
+
(enum_declaration
|
|
743
|
+
name: (identifier) @def.enum.name) @def.enum)
|
|
744
|
+
`;
|
|
745
|
+
const NAME_CAPTURE_RE = /^def\.(\w+)\.name$/;
|
|
746
|
+
const FULL_CAPTURE_RE = /^def\.(\w+)$/;
|
|
747
|
+
async function extractTypeScript(opts) {
|
|
748
|
+
const { source, language, repoPath } = opts;
|
|
749
|
+
const parser = new Parser();
|
|
750
|
+
parser.setLanguage(language);
|
|
751
|
+
const tree = parser.parse(source);
|
|
752
|
+
if (!tree) return { symbols: [] };
|
|
753
|
+
const query = new Query(language, TYPESCRIPT_QUERY);
|
|
754
|
+
const matches = query.matches(tree.rootNode);
|
|
755
|
+
const nameByNodeId = /* @__PURE__ */ new Map();
|
|
756
|
+
const kindByNodeId = /* @__PURE__ */ new Map();
|
|
757
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const m of matches) {
|
|
759
|
+
let nameText = null;
|
|
760
|
+
let kindName = null;
|
|
761
|
+
let fullNode = null;
|
|
762
|
+
for (const cap of m.captures) {
|
|
763
|
+
nodeById.set(cap.node.id, cap.node);
|
|
764
|
+
if (NAME_CAPTURE_RE.test(cap.name)) {
|
|
765
|
+
nameText = source.slice(cap.node.startIndex, cap.node.endIndex);
|
|
766
|
+
const k = cap.name.replace("def.", "").replace(".name", "");
|
|
767
|
+
kindName = k;
|
|
768
|
+
} else if (FULL_CAPTURE_RE.test(cap.name)) {
|
|
769
|
+
fullNode = cap.node;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (fullNode && nameText && kindName) {
|
|
773
|
+
nameByNodeId.set(fullNode.id, nameText);
|
|
774
|
+
kindByNodeId.set(fullNode.id, kindName);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const out = [];
|
|
778
|
+
const seen = /* @__PURE__ */ new Set();
|
|
779
|
+
for (const m of matches) {
|
|
780
|
+
let fullNode = null;
|
|
781
|
+
for (const cap of m.captures) {
|
|
782
|
+
nodeById.set(cap.node.id, cap.node);
|
|
783
|
+
if (FULL_CAPTURE_RE.test(cap.name)) {
|
|
784
|
+
fullNode = cap.node;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (!fullNode) continue;
|
|
788
|
+
if (seen.has(fullNode.id)) continue;
|
|
789
|
+
seen.add(fullNode.id);
|
|
790
|
+
const name = nameByNodeId.get(fullNode.id);
|
|
791
|
+
const kind = kindByNodeId.get(fullNode.id);
|
|
792
|
+
if (!name || !kind) continue;
|
|
793
|
+
const sig = source.slice(fullNode.startIndex, Math.min(fullNode.endIndex, fullNode.startIndex + 400)).split("\n").slice(0, 2).join(" ").trim();
|
|
794
|
+
const docstring = extractDocstring(source, fullNode);
|
|
795
|
+
const vis = isExported(fullNode) ? "public" : "internal";
|
|
796
|
+
const cx = approxComplexity$1(fullNode);
|
|
797
|
+
let parentId = null;
|
|
798
|
+
if (kind === "method") {
|
|
799
|
+
let p = fullNode.parent;
|
|
800
|
+
while (p) {
|
|
801
|
+
if (p.type === "class_declaration" || p.type === "class") {
|
|
802
|
+
const pName = nameByNodeId.get(p.id);
|
|
803
|
+
const pKind = kindByNodeId.get(p.id);
|
|
804
|
+
if (pName && pKind === "class") {
|
|
805
|
+
parentId = buildId(repoPath, "class", pName);
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
p = p.parent;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
out.push({
|
|
813
|
+
id: buildId(repoPath, kind, name),
|
|
814
|
+
name,
|
|
815
|
+
kind,
|
|
816
|
+
signature: sig,
|
|
817
|
+
lineRange: [
|
|
818
|
+
fullNode.startPosition.row + 1,
|
|
819
|
+
fullNode.endPosition.row + 1
|
|
820
|
+
],
|
|
821
|
+
docstring,
|
|
822
|
+
parentId,
|
|
823
|
+
visibility: vis,
|
|
824
|
+
complexity: cx
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return { symbols: out };
|
|
828
|
+
}
|
|
829
|
+
function buildId(repoPath, kind, name) {
|
|
830
|
+
return `${repoPath}::${kind}.${name}`;
|
|
831
|
+
}
|
|
832
|
+
function extractDocstring(source, node) {
|
|
833
|
+
let top = node;
|
|
834
|
+
while (top && top.parent && top.parent.type !== "program") {
|
|
835
|
+
top = top.parent;
|
|
836
|
+
}
|
|
837
|
+
if (!top) return null;
|
|
838
|
+
const programRoot = top.parent;
|
|
839
|
+
if (!programRoot) return null;
|
|
840
|
+
let windowStart = 0;
|
|
841
|
+
const maxWindow = 4096;
|
|
842
|
+
let cursor = top.previousNamedSibling;
|
|
843
|
+
let safety = 16;
|
|
844
|
+
while (cursor && safety-- > 0) {
|
|
845
|
+
if (cursor.startIndex > 0) windowStart = cursor.startIndex;
|
|
846
|
+
const dist = top.startIndex - windowStart;
|
|
847
|
+
if (dist > maxWindow) break;
|
|
848
|
+
cursor = cursor.previousNamedSibling;
|
|
849
|
+
}
|
|
850
|
+
const window = source.slice(windowStart, top.startIndex);
|
|
851
|
+
const jsdocMatch = /\/\*\*([\s\S]*?)\*\//.exec(window);
|
|
852
|
+
if (jsdocMatch) {
|
|
853
|
+
const cleaned = jsdocMatch[1].replace(/^\s*\* ?/gm, "").trim();
|
|
854
|
+
if (cleaned) return firstParagraph$1(cleaned, 400);
|
|
855
|
+
}
|
|
856
|
+
const lines = window.split("\n").slice(-8);
|
|
857
|
+
const commentLines = [];
|
|
858
|
+
for (const ln of lines.reverse()) {
|
|
859
|
+
const m = /^\s*\/\/\s?(.*)$/.exec(ln);
|
|
860
|
+
if (!m) {
|
|
861
|
+
if (commentLines.length > 0) break;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
commentLines.unshift(m[1] ?? "");
|
|
865
|
+
}
|
|
866
|
+
if (commentLines.length === 0) return null;
|
|
867
|
+
return firstParagraph$1(commentLines.join(" "), 400);
|
|
868
|
+
}
|
|
869
|
+
function firstParagraph$1(text, max) {
|
|
870
|
+
const idx = text.indexOf("\n\n");
|
|
871
|
+
const first = idx >= 0 ? text.slice(0, idx) : text;
|
|
872
|
+
return first.length > max ? `${first.slice(0, max)}\u2026` : first;
|
|
873
|
+
}
|
|
874
|
+
function isExported(node) {
|
|
875
|
+
let p = node.parent;
|
|
876
|
+
while (p) {
|
|
877
|
+
if (p.type === "export_statement") return true;
|
|
878
|
+
p = p.parent;
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
function approxComplexity$1(node) {
|
|
883
|
+
let count = 1;
|
|
884
|
+
walk$2(node, (n) => {
|
|
885
|
+
if (n.type === "if_statement" || n.type === "for_statement" || n.type === "for_in_statement" || n.type === "while_statement" || n.type === "do_statement" || n.type === "case_statement" || n.type === "catch_clause" || n.type === "switch_case") {
|
|
886
|
+
count++;
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return count;
|
|
890
|
+
}
|
|
891
|
+
function walk$2(node, fn) {
|
|
892
|
+
fn(node);
|
|
893
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
894
|
+
const c = node.child(i);
|
|
895
|
+
if (c) walk$2(c, fn);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function extractWithGrammar(source, lang, repoPath) {
|
|
899
|
+
const language = await getLanguage(lang);
|
|
900
|
+
if (!language) return [];
|
|
901
|
+
const { symbols } = await extractTypeScript({ source, language, repoPath });
|
|
902
|
+
return symbols;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const NAME_RE = /^def\.([\w]+)\.name$/;
|
|
906
|
+
const FULL_RE = /^def\.([\w]+)$/;
|
|
907
|
+
async function runGenericExtract(opts) {
|
|
908
|
+
const { source, language, query, builder, repoPath } = opts;
|
|
909
|
+
const parser = new Parser();
|
|
910
|
+
parser.setLanguage(language);
|
|
911
|
+
const tree = parser.parse(source);
|
|
912
|
+
if (!tree) return [];
|
|
913
|
+
const q = new Query(language, query);
|
|
914
|
+
const matches = q.matches(tree.rootNode);
|
|
915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
916
|
+
const out = [];
|
|
917
|
+
for (const m of matches) {
|
|
918
|
+
let fullNode = null;
|
|
919
|
+
let nameNode = null;
|
|
920
|
+
let kindName = null;
|
|
921
|
+
for (const cap of m.captures) {
|
|
922
|
+
if (FULL_RE.test(cap.name)) {
|
|
923
|
+
fullNode = cap.node;
|
|
924
|
+
kindName = cap.name.slice(4);
|
|
925
|
+
} else if (NAME_RE.test(cap.name)) {
|
|
926
|
+
nameNode = cap.node;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (!fullNode || !kindName) continue;
|
|
930
|
+
if (seen.has(fullNode.id)) continue;
|
|
931
|
+
seen.add(fullNode.id);
|
|
932
|
+
const symbolKind = builder.kindOf(kindName);
|
|
933
|
+
if (!symbolKind) continue;
|
|
934
|
+
const raw = {
|
|
935
|
+
fullNode,
|
|
936
|
+
nameNode: nameNode ?? fullNode,
|
|
937
|
+
kindName,
|
|
938
|
+
source,
|
|
939
|
+
repoPath
|
|
940
|
+
};
|
|
941
|
+
const sym = builder.build(raw);
|
|
942
|
+
if (sym) out.push(sym);
|
|
943
|
+
}
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
function makeSignature(source, node, max = 400) {
|
|
947
|
+
const slice = source.slice(node.startIndex, Math.min(node.endIndex, node.startIndex + max));
|
|
948
|
+
return slice.split("\n").slice(0, 2).join(" ").replace(/\s+/g, " ").trim();
|
|
949
|
+
}
|
|
950
|
+
function findDocstringAbove(source, node) {
|
|
951
|
+
const above = findDocstringAboveOnly(source, node);
|
|
952
|
+
if (above) return above;
|
|
953
|
+
const inside = findDocstringInside(source, node);
|
|
954
|
+
return inside;
|
|
955
|
+
}
|
|
956
|
+
function findDocstringAboveOnly(source, node) {
|
|
957
|
+
let top = node;
|
|
958
|
+
while (top && top.parent && top.parent.type !== "program" && top.parent.type !== "translation_unit" && top.parent.type !== "source_file" && top.parent.type !== "module") {
|
|
959
|
+
top = top.parent;
|
|
960
|
+
}
|
|
961
|
+
if (!top) return null;
|
|
962
|
+
let windowStart = 0;
|
|
963
|
+
let cursor = top.previousNamedSibling;
|
|
964
|
+
let safety = 16;
|
|
965
|
+
while (cursor && safety-- > 0) {
|
|
966
|
+
if (cursor.startIndex > 0) windowStart = cursor.startIndex;
|
|
967
|
+
const dist = top.startIndex - windowStart;
|
|
968
|
+
if (dist > 4096) break;
|
|
969
|
+
cursor = cursor.previousNamedSibling;
|
|
970
|
+
}
|
|
971
|
+
const text = source.slice(windowStart, top.startIndex);
|
|
972
|
+
const lines = text.split("\n").slice(-12);
|
|
973
|
+
const jsdoc = /\/\*\*([\s\S]*?)\*\//.exec(text);
|
|
974
|
+
if (jsdoc) {
|
|
975
|
+
const cleaned = jsdoc[1].replace(/^\s*\* ?/gm, "").trim();
|
|
976
|
+
if (cleaned) return firstParagraph(cleaned, 400);
|
|
977
|
+
}
|
|
978
|
+
const comment = /\/\*([\s\S]*?)\*\//.exec(text);
|
|
979
|
+
if (comment) {
|
|
980
|
+
const cleaned = comment[1].replace(/^\s*\* ?/gm, "").trim();
|
|
981
|
+
if (cleaned) return firstParagraph(cleaned, 400);
|
|
982
|
+
}
|
|
983
|
+
const hashLines = [];
|
|
984
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
985
|
+
const ln = lines[i] ?? "";
|
|
986
|
+
const m = /^\s*#\s?(.*)$/.exec(ln);
|
|
987
|
+
if (!m) {
|
|
988
|
+
if (hashLines.length > 0) break;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
hashLines.unshift(m[1] ?? "");
|
|
992
|
+
}
|
|
993
|
+
if (hashLines.length > 0) return firstParagraph(hashLines.join(" "), 400);
|
|
994
|
+
const slashLines = [];
|
|
995
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
996
|
+
const ln = lines[i] ?? "";
|
|
997
|
+
const m = /^\s*\/\/\s?(.*)$/.exec(ln);
|
|
998
|
+
if (!m) {
|
|
999
|
+
if (slashLines.length > 0) break;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
slashLines.unshift(m[1] ?? "");
|
|
1003
|
+
}
|
|
1004
|
+
if (slashLines.length > 0) return firstParagraph(slashLines.join(" "), 400);
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
function findDocstringInside(source, node) {
|
|
1008
|
+
function visit(n, depth) {
|
|
1009
|
+
if (n.type === "string") {
|
|
1010
|
+
const text = source.slice(n.startIndex, n.endIndex).trim();
|
|
1011
|
+
const cleaned = text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
|
|
1012
|
+
if (cleaned) return firstParagraph(cleaned, 400);
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
if (depth >= 6) return null;
|
|
1016
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1017
|
+
const c = n.namedChild(i);
|
|
1018
|
+
if (c) {
|
|
1019
|
+
const found = visit(c, depth + 1);
|
|
1020
|
+
if (found) return found;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
return visit(node, 0);
|
|
1026
|
+
}
|
|
1027
|
+
function isRubyPrivate(source, node) {
|
|
1028
|
+
let top = node;
|
|
1029
|
+
while (top && top.parent) {
|
|
1030
|
+
const pt = top.parent.type;
|
|
1031
|
+
if (pt === "class" || pt === "module" || pt === "program" || pt === "source_file") break;
|
|
1032
|
+
top = top.parent;
|
|
1033
|
+
}
|
|
1034
|
+
if (!top || !top.parent) return false;
|
|
1035
|
+
const clsStart = top.parent.startIndex;
|
|
1036
|
+
const clsEnd = top.parent.endIndex;
|
|
1037
|
+
const classBody = source.slice(clsStart, clsEnd);
|
|
1038
|
+
const methodStart = node.startIndex - clsStart;
|
|
1039
|
+
if (methodStart < 0) return false;
|
|
1040
|
+
const head = classBody.slice(0, methodStart);
|
|
1041
|
+
const lines = head.split("\n").slice(-30);
|
|
1042
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1043
|
+
const ln = (lines[i] ?? "").trim();
|
|
1044
|
+
if (ln === "" || ln === "end") continue;
|
|
1045
|
+
if (ln.startsWith("def ") || ln.startsWith("class ") || ln.startsWith("module ")) return false;
|
|
1046
|
+
if (ln === "private" || ln === "protected" || ln.startsWith("private ") || ln.startsWith("protected ")) {
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
function firstParagraph(text, max) {
|
|
1054
|
+
const idx = text.indexOf("\n\n");
|
|
1055
|
+
const first = idx >= 0 ? text.slice(0, idx) : text;
|
|
1056
|
+
return first.length > max ? `${first.slice(0, max)}\u2026` : first;
|
|
1057
|
+
}
|
|
1058
|
+
function approxComplexity(node) {
|
|
1059
|
+
let count = 1;
|
|
1060
|
+
walk$1(node, (n) => {
|
|
1061
|
+
switch (n.type) {
|
|
1062
|
+
case "if_statement":
|
|
1063
|
+
case "elif_clause":
|
|
1064
|
+
case "else_clause":
|
|
1065
|
+
case "for_statement":
|
|
1066
|
+
case "for_in_statement":
|
|
1067
|
+
case "while_statement":
|
|
1068
|
+
case "do_statement":
|
|
1069
|
+
case "case_clause":
|
|
1070
|
+
case "switch_case":
|
|
1071
|
+
case "catch_clause":
|
|
1072
|
+
count++;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
return count;
|
|
1077
|
+
}
|
|
1078
|
+
function walk$1(node, fn) {
|
|
1079
|
+
fn(node);
|
|
1080
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1081
|
+
const c = node.child(i);
|
|
1082
|
+
if (c) walk$1(c, fn);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function makeSymbolId(repoPath, kind, name) {
|
|
1086
|
+
return `${repoPath}::${kind}.${name}`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const PYTHON_QUERY = `
|
|
1090
|
+
(function_definition
|
|
1091
|
+
name: (identifier) @def.function.name) @def.function
|
|
1092
|
+
|
|
1093
|
+
(class_definition
|
|
1094
|
+
name: (identifier) @def.class.name) @def.class
|
|
1095
|
+
`;
|
|
1096
|
+
const pythonBuilder = {
|
|
1097
|
+
kindOf: (k) => k === "function" || k === "class" ? k : null,
|
|
1098
|
+
build: (raw) => buildPublicPython(raw)
|
|
1099
|
+
};
|
|
1100
|
+
function buildPublicPython(raw) {
|
|
1101
|
+
const name = textOfNode(raw.nameNode, raw.source);
|
|
1102
|
+
if (!name) return null;
|
|
1103
|
+
const kind = raw.kindName === "class" ? "class" : "function";
|
|
1104
|
+
const vis = name.startsWith("_") ? "internal" : "public";
|
|
1105
|
+
return makeSymbol(raw, kind, name, vis);
|
|
1106
|
+
}
|
|
1107
|
+
const GO_QUERY = `
|
|
1108
|
+
(function_declaration
|
|
1109
|
+
name: (identifier) @def.function.name) @def.function
|
|
1110
|
+
|
|
1111
|
+
(method_declaration
|
|
1112
|
+
name: (field_identifier) @def.method.name) @def.method
|
|
1113
|
+
|
|
1114
|
+
(type_declaration) @def.type
|
|
1115
|
+
`;
|
|
1116
|
+
const goBuilder = {
|
|
1117
|
+
kindOf: (k) => {
|
|
1118
|
+
if (k === "function" || k === "method") return k;
|
|
1119
|
+
if (k === "type") return "class";
|
|
1120
|
+
return null;
|
|
1121
|
+
},
|
|
1122
|
+
build: (raw) => {
|
|
1123
|
+
const text = textOf(raw.fullNode, raw.source) ?? "";
|
|
1124
|
+
const nameMatch = /(?:^|\n)\s*type\s+([A-Za-z_]\w*)/.exec(text);
|
|
1125
|
+
const name = nameMatch ? nameMatch[1] : textOfNode(raw.nameNode, raw.source);
|
|
1126
|
+
if (!name) return null;
|
|
1127
|
+
const vis = /^[A-Z]/.test(name) ? "public" : "internal";
|
|
1128
|
+
let kind = "type";
|
|
1129
|
+
if (raw.kindName === "function" || raw.kindName === "method") {
|
|
1130
|
+
kind = raw.kindName;
|
|
1131
|
+
} else if (/^\s*type\s+\w+\s+struct\b/.test(" " + text)) {
|
|
1132
|
+
kind = "class";
|
|
1133
|
+
} else if (/^\s*type\s+\w+\s+interface\b/.test(" " + text)) {
|
|
1134
|
+
kind = "interface";
|
|
1135
|
+
} else {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
return makeSymbol(raw, kind, name, vis);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
const RUST_QUERY = `
|
|
1142
|
+
(function_item
|
|
1143
|
+
name: (identifier) @def.function.name) @def.function
|
|
1144
|
+
|
|
1145
|
+
(struct_item
|
|
1146
|
+
name: (type_identifier) @def.class.name) @def.class
|
|
1147
|
+
|
|
1148
|
+
(enum_item
|
|
1149
|
+
name: (type_identifier) @def.enum.name) @def.enum
|
|
1150
|
+
|
|
1151
|
+
(trait_item
|
|
1152
|
+
name: (type_identifier) @def.interface.name) @def.interface
|
|
1153
|
+
|
|
1154
|
+
(impl_item
|
|
1155
|
+
trait: (type_identifier) @def.interface.name) @def.interface
|
|
1156
|
+
`;
|
|
1157
|
+
const rustBuilder = {
|
|
1158
|
+
kindOf: (k) => {
|
|
1159
|
+
const m = {
|
|
1160
|
+
function: "function",
|
|
1161
|
+
class: "class",
|
|
1162
|
+
interface: "interface",
|
|
1163
|
+
enum: "enum"
|
|
1164
|
+
};
|
|
1165
|
+
return m[k] ?? null;
|
|
1166
|
+
},
|
|
1167
|
+
build: (raw) => {
|
|
1168
|
+
const text = textOf(raw.fullNode, raw.source) ?? "";
|
|
1169
|
+
const isPublic = /\bpub\s/.test(text);
|
|
1170
|
+
const vis = isPublic ? "public" : "internal";
|
|
1171
|
+
const name = textOf(raw.nameNode, raw.source);
|
|
1172
|
+
if (!name) return null;
|
|
1173
|
+
const map = {
|
|
1174
|
+
function: "function",
|
|
1175
|
+
class: "class",
|
|
1176
|
+
interface: "interface",
|
|
1177
|
+
enum: "enum"
|
|
1178
|
+
};
|
|
1179
|
+
return makeSymbol(raw, map[raw.kindName], name, vis);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
const JAVA_QUERY = `
|
|
1183
|
+
(method_declaration
|
|
1184
|
+
name: (identifier) @def.method.name) @def.method
|
|
1185
|
+
|
|
1186
|
+
(class_declaration
|
|
1187
|
+
name: (identifier) @def.class.name) @def.class
|
|
1188
|
+
|
|
1189
|
+
(interface_declaration
|
|
1190
|
+
name: (identifier) @def.interface.name) @def.interface
|
|
1191
|
+
|
|
1192
|
+
(enum_declaration
|
|
1193
|
+
name: (identifier) @def.enum.name) @def.enum
|
|
1194
|
+
`;
|
|
1195
|
+
const javaBuilder = {
|
|
1196
|
+
kindOf: (k) => {
|
|
1197
|
+
const m = {
|
|
1198
|
+
method: "method",
|
|
1199
|
+
class: "class",
|
|
1200
|
+
interface: "interface",
|
|
1201
|
+
enum: "enum"
|
|
1202
|
+
};
|
|
1203
|
+
return m[k] ?? null;
|
|
1204
|
+
},
|
|
1205
|
+
build: (raw) => {
|
|
1206
|
+
const text = textOf(raw.fullNode, raw.source) ?? "";
|
|
1207
|
+
const isPublic = /\bpublic\s/.test(text);
|
|
1208
|
+
const vis = isPublic ? "public" : "internal";
|
|
1209
|
+
const name = textOf(raw.nameNode, raw.source);
|
|
1210
|
+
if (!name) return null;
|
|
1211
|
+
const map = {
|
|
1212
|
+
method: "method",
|
|
1213
|
+
class: "class",
|
|
1214
|
+
interface: "interface",
|
|
1215
|
+
enum: "enum"
|
|
1216
|
+
};
|
|
1217
|
+
return makeSymbol(raw, map[raw.kindName], name, vis);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
const C_QUERY = `
|
|
1221
|
+
(function_definition
|
|
1222
|
+
declarator: (function_declarator
|
|
1223
|
+
declarator: (identifier) @def.function.name)) @def.function
|
|
1224
|
+
`;
|
|
1225
|
+
const cBuilder = {
|
|
1226
|
+
kindOf: (k) => k === "function" ? "function" : null,
|
|
1227
|
+
build: (raw) => {
|
|
1228
|
+
const text = textOf(raw.fullNode, raw.source) ?? "";
|
|
1229
|
+
const isStatic = /^static\s/m.test(text);
|
|
1230
|
+
const vis = isStatic ? "internal" : "public";
|
|
1231
|
+
const name = textOf(raw.nameNode, raw.source);
|
|
1232
|
+
if (!name) return null;
|
|
1233
|
+
return makeSymbol(raw, "function", name, vis);
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
const CPP_QUERY = `
|
|
1237
|
+
(function_definition
|
|
1238
|
+
declarator: (function_declarator
|
|
1239
|
+
declarator: (identifier) @def.function.name)) @def.function
|
|
1240
|
+
|
|
1241
|
+
(function_definition
|
|
1242
|
+
declarator: (function_declarator
|
|
1243
|
+
declarator: (field_identifier) @def.method.name)) @def.method
|
|
1244
|
+
|
|
1245
|
+
(class_specifier
|
|
1246
|
+
name: (type_identifier) @def.class.name) @def.class
|
|
1247
|
+
|
|
1248
|
+
(struct_specifier
|
|
1249
|
+
name: (type_identifier) @def.class.name) @def.class
|
|
1250
|
+
|
|
1251
|
+
(class_specifier
|
|
1252
|
+
name: (primitive_type) @def.class.name) @def.class
|
|
1253
|
+
`;
|
|
1254
|
+
const cppBuilder = {
|
|
1255
|
+
kindOf: (k) => {
|
|
1256
|
+
const m = {
|
|
1257
|
+
function: "function",
|
|
1258
|
+
method: "method",
|
|
1259
|
+
class: "class"
|
|
1260
|
+
};
|
|
1261
|
+
return m[k] ?? null;
|
|
1262
|
+
},
|
|
1263
|
+
build: (raw) => {
|
|
1264
|
+
textOf(raw.fullNode, raw.source) ?? "";
|
|
1265
|
+
const isPrivate = /private\s*:/.test(textOfSlice(raw, 250));
|
|
1266
|
+
const vis = isPrivate ? "internal" : "public";
|
|
1267
|
+
const name = textOf(raw.nameNode, raw.source);
|
|
1268
|
+
if (!name) return null;
|
|
1269
|
+
const map = {
|
|
1270
|
+
function: "function",
|
|
1271
|
+
method: "method",
|
|
1272
|
+
class: "class"
|
|
1273
|
+
};
|
|
1274
|
+
return makeSymbol(raw, map[raw.kindName], name, vis);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const RUBY_QUERY = `
|
|
1278
|
+
(method
|
|
1279
|
+
name: (identifier) @def.method.name) @def.method
|
|
1280
|
+
|
|
1281
|
+
(singleton_method
|
|
1282
|
+
name: (identifier) @def.method.name) @def.method
|
|
1283
|
+
|
|
1284
|
+
(class
|
|
1285
|
+
name: (constant) @def.class.name) @def.class
|
|
1286
|
+
|
|
1287
|
+
(module
|
|
1288
|
+
name: (constant) @def.module.name) @def.module
|
|
1289
|
+
`;
|
|
1290
|
+
const rubyBuilder = {
|
|
1291
|
+
kindOf: (k) => {
|
|
1292
|
+
const m = {
|
|
1293
|
+
method: "method",
|
|
1294
|
+
class: "class",
|
|
1295
|
+
module: "module"
|
|
1296
|
+
};
|
|
1297
|
+
return m[k] ?? null;
|
|
1298
|
+
},
|
|
1299
|
+
build: (raw) => {
|
|
1300
|
+
const isPrivate = isRubyPrivate(raw.source, raw.fullNode);
|
|
1301
|
+
const vis = isPrivate ? "internal" : "public";
|
|
1302
|
+
const name = textOfNode(raw.nameNode, raw.source);
|
|
1303
|
+
if (!name) return null;
|
|
1304
|
+
const map = {
|
|
1305
|
+
method: "method",
|
|
1306
|
+
class: "class",
|
|
1307
|
+
module: "module"
|
|
1308
|
+
};
|
|
1309
|
+
return makeSymbol(raw, map[raw.kindName], name, vis);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
async function extractOtherLanguage(source, lang, repoPath) {
|
|
1313
|
+
const language = await getLanguage(lang);
|
|
1314
|
+
if (!language) return [];
|
|
1315
|
+
const spec = {
|
|
1316
|
+
python: { query: PYTHON_QUERY, builder: pythonBuilder },
|
|
1317
|
+
go: { query: GO_QUERY, builder: goBuilder },
|
|
1318
|
+
rust: { query: RUST_QUERY, builder: rustBuilder },
|
|
1319
|
+
java: { query: JAVA_QUERY, builder: javaBuilder },
|
|
1320
|
+
c: { query: C_QUERY, builder: cBuilder },
|
|
1321
|
+
cpp: { query: CPP_QUERY, builder: cppBuilder },
|
|
1322
|
+
ruby: { query: RUBY_QUERY, builder: rubyBuilder }
|
|
1323
|
+
};
|
|
1324
|
+
const s = spec[lang];
|
|
1325
|
+
return runGenericExtract({ source, language, query: s.query, builder: s.builder, repoPath });
|
|
1326
|
+
}
|
|
1327
|
+
function textOf(node, source, kind) {
|
|
1328
|
+
return source.slice(node.startIndex, node.endIndex).trim();
|
|
1329
|
+
}
|
|
1330
|
+
function textOfNode(node, source) {
|
|
1331
|
+
return source.slice(node.startIndex, node.endIndex).trim();
|
|
1332
|
+
}
|
|
1333
|
+
function textOfSlice(raw, before) {
|
|
1334
|
+
const start = Math.max(0, raw.fullNode.startIndex - before);
|
|
1335
|
+
return raw.source.slice(start, raw.fullNode.endIndex);
|
|
1336
|
+
}
|
|
1337
|
+
function makeSymbol(raw, kind, name, visibility) {
|
|
1338
|
+
const sig = makeSignature(raw.source, raw.fullNode);
|
|
1339
|
+
const docstring = findDocstringAbove(raw.source, raw.fullNode);
|
|
1340
|
+
const cx = approxComplexity(raw.fullNode);
|
|
1341
|
+
let finalKind = kind;
|
|
1342
|
+
let parentId = null;
|
|
1343
|
+
let p = raw.fullNode.parent;
|
|
1344
|
+
while (p) {
|
|
1345
|
+
if (p.type === "class_definition" || p.type === "class_declaration" || p.type === "class_specifier" || p.type === "impl_item") {
|
|
1346
|
+
const classNameNode = findFirstIdentifierNode(p);
|
|
1347
|
+
if (classNameNode) {
|
|
1348
|
+
const className = raw.source.slice(classNameNode.startIndex, classNameNode.endIndex).trim();
|
|
1349
|
+
if (className) {
|
|
1350
|
+
parentId = makeSymbolId(raw.repoPath, "class", className);
|
|
1351
|
+
if (kind === "function") finalKind = "method";
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
p = p.parent;
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
id: makeSymbolId(raw.repoPath, finalKind, name),
|
|
1361
|
+
name,
|
|
1362
|
+
kind: finalKind,
|
|
1363
|
+
signature: sig,
|
|
1364
|
+
lineRange: [
|
|
1365
|
+
raw.fullNode.startPosition.row + 1,
|
|
1366
|
+
raw.fullNode.endPosition.row + 1
|
|
1367
|
+
],
|
|
1368
|
+
docstring,
|
|
1369
|
+
parentId,
|
|
1370
|
+
visibility,
|
|
1371
|
+
complexity: cx
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function findFirstIdentifierNode(node) {
|
|
1375
|
+
function visit(n, depth) {
|
|
1376
|
+
if (n.type === "identifier" || n.type === "type_identifier" || n.type === "constant") {
|
|
1377
|
+
return n;
|
|
1378
|
+
}
|
|
1379
|
+
if (depth >= 2) return null;
|
|
1380
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1381
|
+
const c = n.child(i);
|
|
1382
|
+
if (c) {
|
|
1383
|
+
const found = visit(c, depth + 1);
|
|
1384
|
+
if (found) return found;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
return visit(node, 0);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async function extractSymbols(input) {
|
|
1393
|
+
const { source, repoPath, language } = input;
|
|
1394
|
+
if (!source) return [];
|
|
1395
|
+
switch (language) {
|
|
1396
|
+
case "typescript":
|
|
1397
|
+
case "tsx":
|
|
1398
|
+
case "javascript":
|
|
1399
|
+
case "jsx":
|
|
1400
|
+
return extractWithGrammar(source, language, repoPath);
|
|
1401
|
+
case "python":
|
|
1402
|
+
case "go":
|
|
1403
|
+
case "rust":
|
|
1404
|
+
case "java":
|
|
1405
|
+
case "c":
|
|
1406
|
+
case "cpp":
|
|
1407
|
+
case "ruby":
|
|
1408
|
+
return extractOtherLanguage(source, language, repoPath);
|
|
1409
|
+
default:
|
|
1410
|
+
return [];
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function isExtractable(language) {
|
|
1414
|
+
return language === "typescript" || language === "tsx" || language === "javascript" || language === "jsx" || language === "python" || language === "go" || language === "rust" || language === "java" || language === "c" || language === "cpp" || language === "ruby";
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function buildSymbolPage(symbol, fileRepoPath) {
|
|
1418
|
+
const lines = [];
|
|
1419
|
+
lines.push(`# ${symbol.kind}: \`${symbol.name}\``);
|
|
1420
|
+
lines.push("");
|
|
1421
|
+
lines.push(
|
|
1422
|
+
`> Defined in \`${fileRepoPath}\`, lines ${symbol.lineRange[0]}-${symbol.lineRange[1]}.`
|
|
1423
|
+
);
|
|
1424
|
+
lines.push("");
|
|
1425
|
+
lines.push("## Signature");
|
|
1426
|
+
lines.push("");
|
|
1427
|
+
lines.push("```" + inferLang(fileRepoPath));
|
|
1428
|
+
lines.push(symbol.signature || symbol.name);
|
|
1429
|
+
lines.push("```");
|
|
1430
|
+
lines.push("");
|
|
1431
|
+
if (symbol.docstring) {
|
|
1432
|
+
lines.push("## Description");
|
|
1433
|
+
lines.push("");
|
|
1434
|
+
lines.push(symbol.docstring);
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
}
|
|
1437
|
+
lines.push("## Metadata");
|
|
1438
|
+
lines.push("");
|
|
1439
|
+
lines.push(`- **Visibility**: ${symbol.visibility}`);
|
|
1440
|
+
lines.push(`- **Complexity**: ${symbol.complexity}`);
|
|
1441
|
+
lines.push("");
|
|
1442
|
+
const body = lines.join("\n");
|
|
1443
|
+
const tokens = pageStats(body);
|
|
1444
|
+
const pagePath = `symbols/${fileRepoPath}/${symbol.kind}.${symbol.name}.md`;
|
|
1445
|
+
const data = { symbol, fileRepoPath };
|
|
1446
|
+
const page = makePage(
|
|
1447
|
+
"symbol",
|
|
1448
|
+
symbol.id,
|
|
1449
|
+
symbol.name,
|
|
1450
|
+
pagePath,
|
|
1451
|
+
data,
|
|
1452
|
+
{
|
|
1453
|
+
repoPath: fileRepoPath,
|
|
1454
|
+
language: null,
|
|
1455
|
+
module: null,
|
|
1456
|
+
lineRange: symbol.lineRange,
|
|
1457
|
+
symbol: symbol.name,
|
|
1458
|
+
symbolKind: symbol.kind,
|
|
1459
|
+
signature: symbol.signature
|
|
1460
|
+
},
|
|
1461
|
+
false
|
|
1462
|
+
);
|
|
1463
|
+
page.tokens = tokens;
|
|
1464
|
+
return { page, body, tokens };
|
|
1465
|
+
}
|
|
1466
|
+
function renderSymbol(page, body) {
|
|
1467
|
+
return frontmatter(page) + body;
|
|
1468
|
+
}
|
|
1469
|
+
function inferLang(path) {
|
|
1470
|
+
const ext = path.split(".").pop() ?? "";
|
|
1471
|
+
if (ext === "ts" || ext === "tsx") return "typescript";
|
|
1472
|
+
if (ext === "js" || ext === "jsx") return "javascript";
|
|
1473
|
+
if (ext === "py") return "python";
|
|
1474
|
+
if (ext === "go") return "go";
|
|
1475
|
+
if (ext === "rs") return "rust";
|
|
1476
|
+
if (ext === "java") return "java";
|
|
1477
|
+
if (ext === "rb") return "ruby";
|
|
1478
|
+
if (ext === "cpp" || ext === "cc" || ext === "cxx") return "cpp";
|
|
1479
|
+
if (ext === "c" || ext === "h") return "c";
|
|
1480
|
+
if (ext === "hpp" || ext === "hh") return "cpp";
|
|
1481
|
+
return "";
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const WALKERS = {
|
|
1485
|
+
typescript: tsLike,
|
|
1486
|
+
tsx: tsLike,
|
|
1487
|
+
javascript: tsLike,
|
|
1488
|
+
jsx: tsLike,
|
|
1489
|
+
python: pyWalker,
|
|
1490
|
+
go: goWalker,
|
|
1491
|
+
rust: rustWalker,
|
|
1492
|
+
java: javaWalker,
|
|
1493
|
+
c: cppLike,
|
|
1494
|
+
cpp: cppLike,
|
|
1495
|
+
ruby: rubyWalker
|
|
1496
|
+
};
|
|
1497
|
+
const TS_IMPORT_TYPES = /* @__PURE__ */ new Set(["import_statement", "export_statement"]);
|
|
1498
|
+
function tsLike(root, source) {
|
|
1499
|
+
const out = [];
|
|
1500
|
+
walk(root, (n) => {
|
|
1501
|
+
if (!TS_IMPORT_TYPES.has(n.type)) return;
|
|
1502
|
+
let srcText = "";
|
|
1503
|
+
const specs = [];
|
|
1504
|
+
const visit = (m, depth) => {
|
|
1505
|
+
if (!m) return;
|
|
1506
|
+
if (depth > 4) return;
|
|
1507
|
+
if (m.type === "string") {
|
|
1508
|
+
if (srcText === "" && depth <= 2) {
|
|
1509
|
+
srcText = source.slice(m.startIndex, m.endIndex).trim();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (m.type === "import_specifier" || m.type === "export_specifier") {
|
|
1513
|
+
const name = findFirstNamedChildText(m, ["identifier", "type_identifier"], source);
|
|
1514
|
+
if (name) specs.push(name);
|
|
1515
|
+
}
|
|
1516
|
+
for (let i = 0; i < m.childCount; i++) {
|
|
1517
|
+
visit(m.child(i), depth + 1);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
visit(n, 0);
|
|
1521
|
+
if (!srcText) return;
|
|
1522
|
+
out.push({ source: srcText, specifiers: specs, row: n.startPosition.row });
|
|
1523
|
+
});
|
|
1524
|
+
return out;
|
|
1525
|
+
}
|
|
1526
|
+
function pyWalker(root, source) {
|
|
1527
|
+
const out = [];
|
|
1528
|
+
walk(root, (n) => {
|
|
1529
|
+
if (n.type === "import_statement") {
|
|
1530
|
+
const srcText = findFirstNamedChildText(n, ["dotted_name"], source);
|
|
1531
|
+
if (srcText) out.push({ source: srcText, specifiers: [], row: n.startPosition.row });
|
|
1532
|
+
} else if (n.type === "import_from_statement") {
|
|
1533
|
+
let srcText = "";
|
|
1534
|
+
const specs = [];
|
|
1535
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1536
|
+
const c = n.namedChild(i);
|
|
1537
|
+
if (!c) continue;
|
|
1538
|
+
if (c.type === "dotted_name" && !srcText) {
|
|
1539
|
+
srcText = source.slice(c.startIndex, c.endIndex).trim();
|
|
1540
|
+
} else if (c.type === "relative_import") {
|
|
1541
|
+
srcText = source.slice(c.startIndex, c.endIndex).trim();
|
|
1542
|
+
} else if (c.type === "dotted_name" && srcText) {
|
|
1543
|
+
specs.push(source.slice(c.startIndex, c.endIndex).trim());
|
|
1544
|
+
} else if (c.type === "aliased_import") {
|
|
1545
|
+
const name = findFirstNamedChildText(c, ["dotted_name"], source);
|
|
1546
|
+
if (name) specs.push(name);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (!srcText) {
|
|
1550
|
+
const text = source.slice(n.startIndex, n.endIndex);
|
|
1551
|
+
const m = /^from\s+(\S+)/.exec(text);
|
|
1552
|
+
if (m) srcText = m[1] ?? "";
|
|
1553
|
+
}
|
|
1554
|
+
if (!srcText) return;
|
|
1555
|
+
out.push({ source: srcText, specifiers: specs, row: n.startPosition.row });
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
function goWalker(root, source) {
|
|
1561
|
+
const out = [];
|
|
1562
|
+
walk(root, (n) => {
|
|
1563
|
+
if (n.type !== "import_spec") return;
|
|
1564
|
+
let srcText = "";
|
|
1565
|
+
const specs = [];
|
|
1566
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1567
|
+
const c = n.child(i);
|
|
1568
|
+
if (!c) continue;
|
|
1569
|
+
if (c.type === "interpreted_string_literal") {
|
|
1570
|
+
srcText = source.slice(c.startIndex, c.endIndex).trim();
|
|
1571
|
+
} else if (c.type === "identifier" || c.type === "dot") {
|
|
1572
|
+
specs.push(source.slice(c.startIndex, c.endIndex).trim());
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (!srcText) return;
|
|
1576
|
+
out.push({ source: srcText, specifiers: specs, row: n.startPosition.row });
|
|
1577
|
+
});
|
|
1578
|
+
return out;
|
|
1579
|
+
}
|
|
1580
|
+
function rustWalker(root, source) {
|
|
1581
|
+
const out = [];
|
|
1582
|
+
walk(root, (n) => {
|
|
1583
|
+
if (n.type !== "use_declaration") return;
|
|
1584
|
+
const text = source.slice(n.startIndex, n.endIndex);
|
|
1585
|
+
const m = /^use\s+([^;{]+?)\s*(::\s*\{([^}]+)\})?\s*;/.exec(text);
|
|
1586
|
+
if (!m) return;
|
|
1587
|
+
const head = m[1]?.trim() ?? "";
|
|
1588
|
+
const group = m[3]?.trim();
|
|
1589
|
+
if (group) {
|
|
1590
|
+
const parts = group.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1591
|
+
out.push({ source: head, specifiers: parts, row: n.startPosition.row });
|
|
1592
|
+
} else {
|
|
1593
|
+
out.push({ source: head, specifiers: [], row: n.startPosition.row });
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
return out;
|
|
1597
|
+
}
|
|
1598
|
+
function javaWalker(root, source) {
|
|
1599
|
+
const out = [];
|
|
1600
|
+
walk(root, (n) => {
|
|
1601
|
+
if (n.type !== "import_declaration") return;
|
|
1602
|
+
const text = source.slice(n.startIndex, n.endIndex);
|
|
1603
|
+
const m = /^import\s+(?:static\s+)?([\w.]+)/.exec(text);
|
|
1604
|
+
if (!m) return;
|
|
1605
|
+
const lastDot = m[1].lastIndexOf(".");
|
|
1606
|
+
const head = lastDot >= 0 ? m[1].slice(0, lastDot) : "";
|
|
1607
|
+
const tail = lastDot >= 0 ? m[1].slice(lastDot + 1) : m[1];
|
|
1608
|
+
const srcText = head ? `${head}.${tail}` : m[1] ?? "";
|
|
1609
|
+
out.push({
|
|
1610
|
+
source: srcText,
|
|
1611
|
+
specifiers: tail ? [tail] : [],
|
|
1612
|
+
row: n.startPosition.row
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
return out;
|
|
1616
|
+
}
|
|
1617
|
+
function cppLike(root, source) {
|
|
1618
|
+
const out = [];
|
|
1619
|
+
walk(root, (n) => {
|
|
1620
|
+
if (n.type !== "preproc_include") return;
|
|
1621
|
+
const text = source.slice(n.startIndex, n.endIndex);
|
|
1622
|
+
const m = /#\s*include\s+([<"])([^>"]+)[>"]/.exec(text);
|
|
1623
|
+
if (!m) return;
|
|
1624
|
+
const delim = m[1];
|
|
1625
|
+
if (delim === "<") return;
|
|
1626
|
+
const srcText = m[2] ?? "";
|
|
1627
|
+
out.push({ source: srcText, specifiers: [], row: n.startPosition.row });
|
|
1628
|
+
});
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
function rubyWalker(root, source) {
|
|
1632
|
+
const out = [];
|
|
1633
|
+
const IMPORT_METHODS = /* @__PURE__ */ new Set(["require", "require_relative", "load"]);
|
|
1634
|
+
walk(root, (n) => {
|
|
1635
|
+
if (n.type !== "call") return;
|
|
1636
|
+
let methodName = "";
|
|
1637
|
+
let strText = "";
|
|
1638
|
+
const visit = (m) => {
|
|
1639
|
+
if (m.type === "identifier" && !methodName) {
|
|
1640
|
+
methodName = source.slice(m.startIndex, m.endIndex);
|
|
1641
|
+
} else if (m.type === "string") {
|
|
1642
|
+
strText = source.slice(m.startIndex, m.endIndex).trim();
|
|
1643
|
+
}
|
|
1644
|
+
for (let i = 0; i < m.childCount; i++) {
|
|
1645
|
+
const c = m.child(i);
|
|
1646
|
+
if (c) visit(c);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
visit(n);
|
|
1650
|
+
if (!IMPORT_METHODS.has(methodName)) return;
|
|
1651
|
+
if (!strText) return;
|
|
1652
|
+
out.push({ source: strText, specifiers: [], row: n.startPosition.row });
|
|
1653
|
+
});
|
|
1654
|
+
return out;
|
|
1655
|
+
}
|
|
1656
|
+
function walk(node, fn) {
|
|
1657
|
+
fn(node);
|
|
1658
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1659
|
+
const c = node.child(i);
|
|
1660
|
+
if (c) walk(c, fn);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function findFirstNamedChildText(node, want, source) {
|
|
1664
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1665
|
+
const c = node.child(i);
|
|
1666
|
+
if (c && want.includes(c.type)) {
|
|
1667
|
+
return source.slice(c.startIndex, c.endIndex).trim();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
async function extractImports(source, repoPath, lang) {
|
|
1673
|
+
const languageObj = await getLanguage(lang);
|
|
1674
|
+
if (!languageObj) return [];
|
|
1675
|
+
const walker = WALKERS[lang];
|
|
1676
|
+
if (!walker) return [];
|
|
1677
|
+
const parser = new Parser();
|
|
1678
|
+
parser.setLanguage(languageObj);
|
|
1679
|
+
const tree = parser.parse(source);
|
|
1680
|
+
if (!tree) return [];
|
|
1681
|
+
const hits = walker(tree.rootNode, source);
|
|
1682
|
+
const edges = [];
|
|
1683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1684
|
+
for (const hit of hits) {
|
|
1685
|
+
const rawSource = stripQuotes(hit.source.trim());
|
|
1686
|
+
const target = resolveImport(repoPath, rawSource, lang);
|
|
1687
|
+
if (!target) continue;
|
|
1688
|
+
const key = `${repoPath}->${target}|${hit.specifiers.join(",")}`;
|
|
1689
|
+
if (seen.has(key)) continue;
|
|
1690
|
+
seen.add(key);
|
|
1691
|
+
edges.push({ from: repoPath, to: target, specifiers: hit.specifiers });
|
|
1692
|
+
}
|
|
1693
|
+
return edges;
|
|
1694
|
+
}
|
|
1695
|
+
function stripQuotes(s) {
|
|
1696
|
+
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
1697
|
+
return s.slice(1, -1);
|
|
1698
|
+
}
|
|
1699
|
+
return s;
|
|
1700
|
+
}
|
|
1701
|
+
function resolveImport(importerRepoPath, rawSource, lang) {
|
|
1702
|
+
if (!rawSource) return null;
|
|
1703
|
+
if (lang === "python") {
|
|
1704
|
+
if (rawSource.startsWith(".")) return resolveRelative(importerRepoPath, rawSource);
|
|
1705
|
+
return rawSource.replace(/\./g, "/");
|
|
1706
|
+
}
|
|
1707
|
+
if (lang === "c" || lang === "cpp") {
|
|
1708
|
+
if (rawSource.startsWith('"')) return resolveRelative(importerRepoPath, rawSource.slice(1, -1));
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
if (lang === "java") {
|
|
1712
|
+
return rawSource.replace(/\./g, "/") + ".java";
|
|
1713
|
+
}
|
|
1714
|
+
if (lang === "ruby" && rawSource.startsWith(".")) {
|
|
1715
|
+
return resolveRelative(importerRepoPath, rawSource);
|
|
1716
|
+
}
|
|
1717
|
+
if (rawSource.startsWith(".") || rawSource.startsWith("/")) {
|
|
1718
|
+
return resolveRelative(importerRepoPath, rawSource);
|
|
1719
|
+
}
|
|
1720
|
+
return rawSource;
|
|
1721
|
+
}
|
|
1722
|
+
function resolveRelative(importerRepoPath, rel) {
|
|
1723
|
+
let normalizedRel = rel;
|
|
1724
|
+
if (/^\.+$/.test(rel) || /^\.+[\w]/.test(rel)) {
|
|
1725
|
+
const dots = (rel.match(/^\.+/)?.[0] ?? "").length;
|
|
1726
|
+
const base = rel.slice(dots);
|
|
1727
|
+
const importerDir2 = path__default.posix.dirname(importerRepoPath);
|
|
1728
|
+
let dir = importerDir2;
|
|
1729
|
+
for (let i = 1; i < dots; i++) {
|
|
1730
|
+
const parent = path__default.posix.dirname(dir);
|
|
1731
|
+
if (parent === dir) return null;
|
|
1732
|
+
dir = parent;
|
|
1733
|
+
}
|
|
1734
|
+
if (!base) return toPosix(dir);
|
|
1735
|
+
normalizedRel = `${dir}/${base}`.replace(/^\.?\//, "");
|
|
1736
|
+
return toPosix(normalizedRel);
|
|
1737
|
+
}
|
|
1738
|
+
const importerDir = path__default.posix.dirname(importerRepoPath);
|
|
1739
|
+
const normalized = path__default.posix.normalize(path__default.posix.join(importerDir, rel));
|
|
1740
|
+
if (normalized.startsWith("..") || normalized === "") return null;
|
|
1741
|
+
return toPosix(normalized);
|
|
1742
|
+
}
|
|
1743
|
+
function computeDependents(edges) {
|
|
1744
|
+
const map = {};
|
|
1745
|
+
for (const e of edges) {
|
|
1746
|
+
const list = map[e.to] ?? [];
|
|
1747
|
+
if (!list.includes(e.from)) list.push(e.from);
|
|
1748
|
+
map[e.to] = list;
|
|
1749
|
+
}
|
|
1750
|
+
return map;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
async function buildWiki(opts) {
|
|
1754
|
+
const t0 = Date.now();
|
|
1755
|
+
const cfg = opts.config ?? await loadConfig(opts.cwd, opts.configPath);
|
|
1756
|
+
const root = opts.cwd;
|
|
1757
|
+
const outRel = cfg.outDir;
|
|
1758
|
+
const outAbs = path__default.resolve(root, outRel);
|
|
1759
|
+
const outNew = `${outAbs}.new`;
|
|
1760
|
+
const log_ = child({ phase: "build", root, out: outRel });
|
|
1761
|
+
log_.info({ cfg: configSummary(cfg) }, "starting build");
|
|
1762
|
+
const walked = await walkFiles({
|
|
1763
|
+
root,
|
|
1764
|
+
ignoreGlobs: cfg.ignore,
|
|
1765
|
+
includeExtensions: cfg.includeExtensions
|
|
1766
|
+
});
|
|
1767
|
+
log_.info({ count: walked.length }, "walked files");
|
|
1768
|
+
const files = [];
|
|
1769
|
+
const languages = /* @__PURE__ */ new Set();
|
|
1770
|
+
for (const w of walked) {
|
|
1771
|
+
if (w.repoPath.startsWith(".codewiki/") || w.repoPath.startsWith(".git/")) continue;
|
|
1772
|
+
if (!w.language) continue;
|
|
1773
|
+
languages.add(w.language);
|
|
1774
|
+
let content = "";
|
|
1775
|
+
try {
|
|
1776
|
+
content = await promises.readFile(w.absPath, "utf8");
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
log_.debug({ path: w.repoPath, err: String(err) }, "failed to read");
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
const hash = sha256Hex(content);
|
|
1782
|
+
let symbols = [];
|
|
1783
|
+
if (isExtractable(w.language)) {
|
|
1784
|
+
try {
|
|
1785
|
+
symbols = await extractSymbols({
|
|
1786
|
+
source: content,
|
|
1787
|
+
repoPath: w.repoPath,
|
|
1788
|
+
language: w.language
|
|
1789
|
+
});
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
log_.debug({ path: w.repoPath, err: String(err) }, "extractor failed");
|
|
1792
|
+
symbols = [];
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
const publicSymbols = symbols.filter((s) => s.visibility === "public");
|
|
1796
|
+
const summary = makeStaticFileSummary(content, w.repoPath, symbols, publicSymbols);
|
|
1797
|
+
files.push({
|
|
1798
|
+
repoPath: w.repoPath,
|
|
1799
|
+
absPath: w.absPath,
|
|
1800
|
+
language: w.language,
|
|
1801
|
+
size: w.size,
|
|
1802
|
+
mtimeMs: w.mtimeMs,
|
|
1803
|
+
contentHash: hash,
|
|
1804
|
+
symbols,
|
|
1805
|
+
publicSymbols,
|
|
1806
|
+
oneLineSummary: summary
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
const modules = groupModules(files, cfg);
|
|
1810
|
+
const visibleModuleCount = [...modules.keys()].filter(
|
|
1811
|
+
(id) => !cfg.modules.ignore.includes(id)
|
|
1812
|
+
).length;
|
|
1813
|
+
const filePagePaths = /* @__PURE__ */ new Map();
|
|
1814
|
+
const moduleTokens = /* @__PURE__ */ new Map();
|
|
1815
|
+
const symbolCount = files.reduce((a, f) => a + f.symbols.length, 0);
|
|
1816
|
+
const meta = {
|
|
1817
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1818
|
+
generator: "code-wiki",
|
|
1819
|
+
generatorVersion: "0.1.0",
|
|
1820
|
+
git: { head: "uncommitted", branch: null, dirty: true, remote: null },
|
|
1821
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1822
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1823
|
+
durationMs: 0,
|
|
1824
|
+
languages: [...languages],
|
|
1825
|
+
fileCount: files.length,
|
|
1826
|
+
moduleCount: visibleModuleCount,
|
|
1827
|
+
symbolCount,
|
|
1828
|
+
models: {
|
|
1829
|
+
staticSummaryVersion: "static-v1",
|
|
1830
|
+
llmSummaryModel: null,
|
|
1831
|
+
llmSummaryModelModule: null
|
|
1832
|
+
},
|
|
1833
|
+
options: {
|
|
1834
|
+
enrichment: cfg.enrichment.enabled,
|
|
1835
|
+
concurrency: cfg.enrichment.concurrency
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const importEdges = [];
|
|
1839
|
+
for (const f of files) {
|
|
1840
|
+
if (!f.language) continue;
|
|
1841
|
+
let content = "";
|
|
1842
|
+
try {
|
|
1843
|
+
content = await promises.readFile(f.absPath, "utf8");
|
|
1844
|
+
} catch {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const edges = await extractImports(content, f.repoPath, f.language);
|
|
1848
|
+
importEdges.push(...edges);
|
|
1849
|
+
}
|
|
1850
|
+
const dependents = computeDependents(importEdges);
|
|
1851
|
+
const arch = buildArchitecturePage(meta, files, cfg, importEdges, dependents);
|
|
1852
|
+
await writeText(outNew, "architecture.md", renderArchitecture(arch.page, arch.body));
|
|
1853
|
+
for (const f of files) {
|
|
1854
|
+
const fp = buildFilePage(f);
|
|
1855
|
+
filePagePaths.set(f.repoPath, fp.page.path);
|
|
1856
|
+
const safePath = path__default.join(outNew, fp.page.path);
|
|
1857
|
+
await promises.mkdir(path__default.dirname(safePath), { recursive: true });
|
|
1858
|
+
await promises.writeFile(safePath, renderFile(fp.page, fp.body), "utf8");
|
|
1859
|
+
for (const s of f.symbols) {
|
|
1860
|
+
const sp = buildSymbolPage(s, f.repoPath);
|
|
1861
|
+
const symPath = path__default.join(outNew, sp.page.path);
|
|
1862
|
+
await promises.mkdir(path__default.dirname(symPath), { recursive: true });
|
|
1863
|
+
await promises.writeFile(symPath, renderSymbol(sp.page, sp.body), "utf8");
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
for (const [id, m] of modules) {
|
|
1867
|
+
const mp = buildModulePage(m, files, filePagePaths, cfg, importEdges, dependents);
|
|
1868
|
+
moduleTokens.set(id, mp.tokens);
|
|
1869
|
+
const safePath = path__default.join(outNew, "modules", `${id}.md`);
|
|
1870
|
+
await promises.mkdir(path__default.dirname(safePath), { recursive: true });
|
|
1871
|
+
await promises.writeFile(safePath, renderModule(mp.page, mp.body), "utf8");
|
|
1872
|
+
}
|
|
1873
|
+
const index = buildIndexPage(meta, modules, pagePathMap(modules, files, filePagePaths), moduleTokens, [], cfg);
|
|
1874
|
+
await writeText(outNew, "INDEX.md", renderIndex(index.page, index.body));
|
|
1875
|
+
meta.durationMs = Date.now() - t0;
|
|
1876
|
+
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1877
|
+
await writeJson(path__default.join(outNew, ".meta.json"), meta);
|
|
1878
|
+
const tree = buildIndexTree(files, modules, filePagePaths, moduleTokens);
|
|
1879
|
+
await writeJson(path__default.join(outNew, ".index.json"), tree);
|
|
1880
|
+
await writeJson(path__default.join(outNew, ".graph.json"), {
|
|
1881
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1882
|
+
imports: importEdges,
|
|
1883
|
+
calls: [],
|
|
1884
|
+
dependents
|
|
1885
|
+
});
|
|
1886
|
+
await replaceDir(outAbs, outNew);
|
|
1887
|
+
log_.info(
|
|
1888
|
+
{ files: files.length, modules: visibleModuleCount, durationMs: meta.durationMs },
|
|
1889
|
+
"build complete"
|
|
1890
|
+
);
|
|
1891
|
+
return {
|
|
1892
|
+
meta,
|
|
1893
|
+
root,
|
|
1894
|
+
outDir: outRel,
|
|
1895
|
+
durationMs: meta.durationMs,
|
|
1896
|
+
fileCount: files.length,
|
|
1897
|
+
moduleCount: visibleModuleCount,
|
|
1898
|
+
symbolCount
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function pagePathMap(modules, files, filePagePaths) {
|
|
1902
|
+
return filePagePaths;
|
|
1903
|
+
}
|
|
1904
|
+
function buildIndexTree(files, modules, filePagePaths, moduleTokens) {
|
|
1905
|
+
const tree = {
|
|
1906
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1907
|
+
modules: [...modules.entries()].filter(([id]) => id !== "_tests").map(([id, m]) => ({
|
|
1908
|
+
id,
|
|
1909
|
+
title: m.title,
|
|
1910
|
+
path: `modules/${id}.md`,
|
|
1911
|
+
files: m.files,
|
|
1912
|
+
tokens: moduleTokens.get(id) ?? 0
|
|
1913
|
+
})),
|
|
1914
|
+
files: files.map((f) => ({
|
|
1915
|
+
path: f.repoPath,
|
|
1916
|
+
module: idForRepoPath(f.repoPath, modules),
|
|
1917
|
+
kind: "source",
|
|
1918
|
+
language: f.language,
|
|
1919
|
+
tokens: estimateFileTokens(f),
|
|
1920
|
+
symbols: f.symbols.length,
|
|
1921
|
+
stale: false,
|
|
1922
|
+
pagePath: filePagePaths.get(f.repoPath) ?? `files/${f.repoPath}.md`
|
|
1923
|
+
})),
|
|
1924
|
+
symbols: files.flatMap(
|
|
1925
|
+
(f) => f.symbols.map((s) => ({
|
|
1926
|
+
id: s.id,
|
|
1927
|
+
name: s.name,
|
|
1928
|
+
kind: s.kind,
|
|
1929
|
+
file: f.repoPath,
|
|
1930
|
+
lineRange: s.lineRange,
|
|
1931
|
+
pagePath: `symbols/${f.repoPath}/${s.kind}.${s.name}.md`,
|
|
1932
|
+
tokens: 150
|
|
1933
|
+
}))
|
|
1934
|
+
)
|
|
1935
|
+
};
|
|
1936
|
+
return tree;
|
|
1937
|
+
}
|
|
1938
|
+
function idForRepoPath(repoPath, modules) {
|
|
1939
|
+
for (const [id, m] of modules) {
|
|
1940
|
+
if (m.files.includes(repoPath)) return id;
|
|
1941
|
+
}
|
|
1942
|
+
return "_root";
|
|
1943
|
+
}
|
|
1944
|
+
function estimateFileTokens(f) {
|
|
1945
|
+
return Math.max(50, Math.round(f.size / 4));
|
|
1946
|
+
}
|
|
1947
|
+
function makeStaticFileSummary(content, repoPath, symbols = [], publicSymbols = []) {
|
|
1948
|
+
const fname = repoPath.split("/").pop() ?? repoPath;
|
|
1949
|
+
const lines = content.split(/\r?\n/);
|
|
1950
|
+
let firstNonBlank = "";
|
|
1951
|
+
for (const ln of lines) {
|
|
1952
|
+
const t = ln.trim();
|
|
1953
|
+
if (t && !t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*")) {
|
|
1954
|
+
firstNonBlank = t;
|
|
1955
|
+
break;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const lc = lines.length;
|
|
1959
|
+
const parts = [`${lc} lines`];
|
|
1960
|
+
if (symbols.length > 0) {
|
|
1961
|
+
parts.push(`${symbols.length} symbols (${publicSymbols.length} public)`);
|
|
1962
|
+
}
|
|
1963
|
+
if (firstNonBlank) parts.push(`starts: \`${firstNonBlank.slice(0, 80)}\``);
|
|
1964
|
+
return `${fname}: ${parts.join(", ")}.`;
|
|
1965
|
+
}
|
|
1966
|
+
async function writeText(outDir, relPath, content) {
|
|
1967
|
+
await atomicWriteText(path__default.join(outDir, relPath), content);
|
|
1968
|
+
}
|
|
1969
|
+
async function replaceDir(target, replacement) {
|
|
1970
|
+
if (await exists(target)) {
|
|
1971
|
+
await promises.rm(target, { recursive: true, force: true });
|
|
1972
|
+
}
|
|
1973
|
+
await promises.rename(replacement, target);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
export { walker as a, buildWiki as b, countTokens as c, detectLanguage as d, groupModules as g, moduleForFile as m, truncateToTokens as t, walkFiles as w };
|