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.
Files changed (41) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +17 -0
  3. package/README.md +130 -0
  4. package/dist/CHANGELOG.md +34 -0
  5. package/dist/LICENSE +17 -0
  6. package/dist/README.md +130 -0
  7. package/dist/chunks/build.mjs +1976 -0
  8. package/dist/chunks/config.mjs +174 -0
  9. package/dist/chunks/llm.mjs +214 -0
  10. package/dist/code-wiki-mcp.mjs +367 -0
  11. package/dist/code-wiki.mjs +11 -0
  12. package/dist/codewiki.mjs +2002 -0
  13. package/dist/grammars/c.wasm +0 -0
  14. package/dist/grammars/cpp.wasm +0 -0
  15. package/dist/grammars/go.wasm +0 -0
  16. package/dist/grammars/java.wasm +0 -0
  17. package/dist/grammars/javascript.wasm +0 -0
  18. package/dist/grammars/python.wasm +0 -0
  19. package/dist/grammars/ruby.wasm +0 -0
  20. package/dist/grammars/rust.wasm +0 -0
  21. package/dist/grammars/tsx.wasm +0 -0
  22. package/dist/grammars/typescript.wasm +0 -0
  23. package/dist/plugin/.mcp.json +11 -0
  24. package/dist/plugin/CLAUDE.md +28 -0
  25. package/dist/plugin/commands/wiki-architecture.md +10 -0
  26. package/dist/plugin/commands/wiki-build.md +23 -0
  27. package/dist/plugin/commands/wiki-enrich.md +22 -0
  28. package/dist/plugin/commands/wiki-graph.md +16 -0
  29. package/dist/plugin/commands/wiki-help.md +49 -0
  30. package/dist/plugin/commands/wiki-refresh.md +13 -0
  31. package/dist/plugin/commands/wiki-search.md +18 -0
  32. package/dist/plugin/commands/wiki-symbol.md +21 -0
  33. package/dist/plugin/hooks/hooks.json +74 -0
  34. package/dist/plugin/hooks/nudge.mjs +197 -0
  35. package/dist/plugin/hooks/post-read-reminder.mjs +56 -0
  36. package/dist/plugin/hooks/post-tool-use.mjs +60 -0
  37. package/dist/plugin/hooks/pre-compact.mjs +25 -0
  38. package/dist/plugin/hooks/session-start.mjs +38 -0
  39. package/dist/plugin/hooks/user-prompt-submit.mjs +28 -0
  40. package/dist/plugin/manifest.json +54 -0
  41. 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 };