@aprimediet/codewalker 1.1.0 → 1.3.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/src/indexer.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
- import { openDb, upsertSymbol, deleteFileSymbols, deleteFile, setMeta, getMeta } from "./db.ts";
11
+ import { openDb, upsertSymbol, deleteFileSymbols, deleteFile, setMeta, getMeta, rebuildFtsIndexes } from "./db.ts";
12
12
  import { detectCtags, runCtags, runCtagsOnFile } from "./extract/ctags.ts";
13
13
  import { parseCtagsOutput } from "./extract/ctags-parse.ts";
14
14
  import { extractRegex } from "./extract/regex.ts";
@@ -83,6 +83,11 @@ export async function scan(options: ScanOptions): Promise<void> {
83
83
  const db = openDb(dbPath);
84
84
 
85
85
  try {
86
+ // Heal any stale/legacy FTS index before the per-row deletes below fire their triggers.
87
+ // Without this, a DB from an older build can corrupt mid-scan ("database disk image is
88
+ // malformed"). See rebuildFtsIndexes() for the full rationale.
89
+ rebuildFtsIndexes(db);
90
+
86
91
  // Start transaction
87
92
  db.exec("BEGIN TRANSACTION");
88
93
 
@@ -150,6 +155,9 @@ export async function sync(options: ScanOptions): Promise<void> {
150
155
  const db = openDb(dbPath);
151
156
 
152
157
  try {
158
+ // Heal a stale/legacy FTS index before any trigger-driven deletes (see rebuildFtsIndexes).
159
+ rebuildFtsIndexes(db);
160
+
153
161
  const lastCommit = getMeta(db, "last_indexed_commit");
154
162
  let changedFiles: string[] = [];
155
163
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tests for libs/cards.ts — lib symbol card rendering.
3
+ *
4
+ * PURE unit tests — no I/O.
5
+ */
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { renderLibCard } from "./cards.ts";
9
+ import { parseCard } from "../cards.ts";
10
+ import type { LibSymbol } from "../types.ts";
11
+
12
+ function makeLibSymbol(overrides: Partial<LibSymbol> = {}): LibSymbol {
13
+ return {
14
+ lib: "hono",
15
+ version: "4.6.3",
16
+ name: "createMiddleware",
17
+ kind: "function",
18
+ signature: "export declare function createMiddleware<E>(handler: Hono): MiddlewareHandler",
19
+ doc: "Define a typed middleware handler.\nUse this to wrap Hono handlers with middleware support.",
20
+ summary: "Define a typed middleware handler.",
21
+ card_path: "",
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe("renderLibCard", () => {
27
+ it("includes lib and version in the frontmatter head", () => {
28
+ const sym = makeLibSymbol();
29
+ const card = renderLibCard(sym);
30
+ expect(card).toContain("lib: hono");
31
+ expect(card).toContain("version: 4.6.3");
32
+ });
33
+
34
+ it("includes name, kind, signature, summary in head", () => {
35
+ const sym = makeLibSymbol();
36
+ const card = renderLibCard(sym);
37
+ expect(card).toContain("name: createMiddleware");
38
+ expect(card).toContain("kind: function");
39
+ expect(card).toContain("signature:");
40
+ expect(card).toContain("summary:");
41
+ });
42
+
43
+ it("has a markdown body with the symbol name as heading", () => {
44
+ const sym = makeLibSymbol();
45
+ const card = renderLibCard(sym);
46
+ expect(card).toContain("# createMiddleware");
47
+ });
48
+
49
+ it("includes doc/body text after the frontmatter", () => {
50
+ const sym = makeLibSymbol();
51
+ const card = renderLibCard(sym);
52
+ expect(card).toContain("Use this to wrap Hono handlers");
53
+ });
54
+
55
+ it("round-trips via parseCard (head fields are preserved)", () => {
56
+ const sym = makeLibSymbol();
57
+ const card = renderLibCard(sym);
58
+ const parsed = parseCard(card);
59
+ expect(parsed).not.toBeNull();
60
+ expect(parsed!.head.name).toBe("createMiddleware");
61
+ expect(parsed!.head.kind).toBe("function");
62
+ expect(parsed!.head.summary).toContain("Define a typed middleware handler");
63
+ });
64
+
65
+ it("produces a module-kind card for README-only packages (no signature)", () => {
66
+ const sym = makeLibSymbol({
67
+ name: "express",
68
+ kind: "module",
69
+ signature: "",
70
+ doc: "Express web framework.",
71
+ summary: "Express web framework.",
72
+ });
73
+ const card = renderLibCard(sym);
74
+ expect(card).toContain("name: express");
75
+ expect(card).toContain("kind: module");
76
+ expect(card).toContain("# express");
77
+ expect(card).toContain("Express web framework");
78
+ });
79
+
80
+ it("handles empty doc gracefully", () => {
81
+ const sym = makeLibSymbol({ doc: "", summary: "" });
82
+ const card = renderLibCard(sym);
83
+ expect(card).toContain("name: createMiddleware");
84
+ expect(card).not.toContain("undefined");
85
+ });
86
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Lib symbol card rendering for codewalker v1.2.
3
+ *
4
+ * PURE module — no I/O. Renders a LibSymbol into a markdown card
5
+ * with frontmatter head (includes lib and version) and JSDoc body.
6
+ */
7
+
8
+ import type { LibSymbol } from "../types.ts";
9
+ import * as path from "node:path";
10
+
11
+ /**
12
+ * Render a LibSymbol into a markdown card string.
13
+ *
14
+ * Frontmatter head includes v1.1 fields PLUS lib/version:
15
+ * ---
16
+ * name: createMiddleware
17
+ * kind: function
18
+ * lib: hono
19
+ * version: 4.6.3
20
+ * signature: export declare function ...
21
+ * location: hono/dist/helper.d.ts (dts_path if available)
22
+ * summary: ...
23
+ * ---
24
+ */
25
+ export function renderLibCard(sym: LibSymbol): string {
26
+ const lines: string[] = ["---"];
27
+
28
+ addHeadField(lines, "name", sym.name);
29
+ addHeadField(lines, "kind", sym.kind);
30
+ addHeadField(lines, "lib", sym.lib);
31
+ addHeadField(lines, "version", sym.version);
32
+ if (sym.signature) addHeadField(lines, "signature", sym.signature);
33
+ if (sym.summary) addHeadField(lines, "summary", sym.summary);
34
+
35
+ lines.push("---");
36
+ lines.push("");
37
+
38
+ // Body
39
+ lines.push(`# ${sym.name}`);
40
+ lines.push("");
41
+
42
+ if (sym.doc) {
43
+ lines.push(sym.doc);
44
+ }
45
+
46
+ return lines.join("\n") + "\n";
47
+ }
48
+
49
+ function addHeadField(lines: string[], key: string, value: string): void {
50
+ // Ensure value doesn't break frontmatter
51
+ const safe = value.replace(/\n/g, " ").trim();
52
+ lines.push(`${key}: ${safe}`);
53
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Tests for libs/dts.ts — .d.ts symbol extraction.
3
+ *
4
+ * These are PURE unit tests (fixture strings only, no I/O).
5
+ * Test every export form listed in §10 of the v1.2 plan.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { extractDtsSymbols } from "./dts.ts";
10
+ import type { LibSymbol } from "../types.ts";
11
+
12
+ /**
13
+ * Helper: run extractDtsSymbols and return sorted results for deterministic assertions.
14
+ */
15
+ function extract(source: string, lib = "test-lib", version = "1.0.0"): LibSymbol[] {
16
+ return extractDtsSymbols(source, lib, version).sort((a, b) => a.name.localeCompare(b.name));
17
+ }
18
+
19
+ describe("extractDtsSymbols", () => {
20
+ // ── function forms ────────────────────────────────────────────
21
+ it("extracts `export declare function`", () => {
22
+ const src = `export declare function createMiddleware<E>(handler: Hono): MiddlewareHandler;\n`;
23
+ const result = extract(src);
24
+ expect(result).toHaveLength(1);
25
+ expect(result[0]!.name).toBe("createMiddleware");
26
+ expect(result[0]!.kind).toBe("function");
27
+ expect(result[0]!.signature).toContain("createMiddleware");
28
+ expect(result[0]!.lib).toBe("test-lib");
29
+ expect(result[0]!.version).toBe("1.0.0");
30
+ });
31
+
32
+ it("extracts `export function` (without declare)", () => {
33
+ const src = `export function greet(name: string): string;\n`;
34
+ const result = extract(src);
35
+ expect(result).toHaveLength(1);
36
+ expect(result[0]!.name).toBe("greet");
37
+ expect(result[0]!.kind).toBe("function");
38
+ });
39
+
40
+ it("extracts generic function `export function f<T>(...)`", () => {
41
+ const src = `export function identity<T>(arg: T): T;\n`;
42
+ const result = extract(src);
43
+ expect(result).toHaveLength(1);
44
+ expect(result[0]!.name).toBe("identity");
45
+ expect(result[0]!.kind).toBe("function");
46
+ });
47
+
48
+ // ── const forms ───────────────────────────────────────────────
49
+ it("extracts `export declare const`", () => {
50
+ const src = `export declare const BASE_URL: string;\n`;
51
+ const result = extract(src);
52
+ expect(result).toHaveLength(1);
53
+ expect(result[0]!.name).toBe("BASE_URL");
54
+ expect(result[0]!.kind).toBe("const");
55
+ });
56
+
57
+ it("extracts `export const` (without declare)", () => {
58
+ const src = `export const TIMEOUT_MS: number = 5000;\n`;
59
+ const result = extract(src);
60
+ expect(result).toHaveLength(1);
61
+ expect(result[0]!.name).toBe("TIMEOUT_MS");
62
+ expect(result[0]!.kind).toBe("const");
63
+ });
64
+
65
+ // ── class forms ───────────────────────────────────────────────
66
+ it("extracts `export declare class`", () => {
67
+ const src = `export declare class Router { handle(): void; }\n`;
68
+ const result = extract(src);
69
+ expect(result).toHaveLength(1);
70
+ expect(result[0]!.name).toBe("Router");
71
+ expect(result[0]!.kind).toBe("class");
72
+ expect(result[0]!.signature).toContain("Router");
73
+ });
74
+
75
+ it("extracts `export class` (without declare)", () => {
76
+ const src = `export class Database { constructor(path: string); }\n`;
77
+ const result = extract(src);
78
+ expect(result).toHaveLength(1);
79
+ expect(result[0]!.name).toBe("Database");
80
+ expect(result[0]!.kind).toBe("class");
81
+ });
82
+
83
+ it("extracts `export abstract class`", () => {
84
+ const src = `export abstract class Serializer { abstract serialize(data: unknown): string; }\n`;
85
+ const result = extract(src);
86
+ expect(result).toHaveLength(1);
87
+ expect(result[0]!.name).toBe("Serializer");
88
+ expect(result[0]!.kind).toBe("class");
89
+ });
90
+
91
+ // ── interface ─────────────────────────────────────────────────
92
+ it("extracts `export interface`", () => {
93
+ const src = `export interface User { id: string; name: string; }\n`;
94
+ const result = extract(src);
95
+ expect(result).toHaveLength(1);
96
+ expect(result[0]!.name).toBe("User");
97
+ expect(result[0]!.kind).toBe("interface");
98
+ });
99
+
100
+ // ── type ──────────────────────────────────────────────────────
101
+ it("extracts `export type`", () => {
102
+ const src = `export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\n`;
103
+ const result = extract(src);
104
+ expect(result).toHaveLength(1);
105
+ expect(result[0]!.name).toBe("JsonValue");
106
+ expect(result[0]!.kind).toBe("type");
107
+ });
108
+
109
+ it("extracts `export type` with generic parameter", () => {
110
+ const src = `export type Result<T> = { ok: true; value: T } | { ok: false; error: string };\n`;
111
+ const result = extract(src);
112
+ expect(result).toHaveLength(1);
113
+ expect(result[0]!.name).toBe("Result");
114
+ expect(result[0]!.kind).toBe("type");
115
+ });
116
+
117
+ // ── enum ──────────────────────────────────────────────────────
118
+ it("extracts `export declare enum`", () => {
119
+ const src = `export declare enum Color { Red, Green, Blue }\n`;
120
+ const result = extract(src);
121
+ expect(result).toHaveLength(1);
122
+ expect(result[0]!.name).toBe("Color");
123
+ expect(result[0]!.kind).toBe("enum");
124
+ });
125
+
126
+ it("extracts `export enum` (without declare)", () => {
127
+ const src = `export enum Direction { Up, Down, Left, Right }\n`;
128
+ const result = extract(src);
129
+ expect(result).toHaveLength(1);
130
+ expect(result[0]!.name).toBe("Direction");
131
+ expect(result[0]!.kind).toBe("enum");
132
+ });
133
+
134
+ // ── namespace ─────────────────────────────────────────────────
135
+ it("extracts `export declare namespace`", () => {
136
+ const src = `export declare namespace Util { function parse(s: string): any; }\n`;
137
+ const result = extract(src);
138
+ expect(result).toHaveLength(1);
139
+ expect(result[0]!.name).toBe("Util");
140
+ expect(result[0]!.kind).toBe("namespace");
141
+ });
142
+
143
+ it("extracts `export namespace` (without declare)", () => {
144
+ const src = `export namespace Config { const port: number; }\n`;
145
+ const result = extract(src);
146
+ expect(result).toHaveLength(1);
147
+ expect(result[0]!.name).toBe("Config");
148
+ expect(result[0]!.kind).toBe("namespace");
149
+ });
150
+
151
+ // ── re-exports ────────────────────────────────────────────────
152
+ it("extracts re-export `export { a, b as c } from \"...\"`", () => {
153
+ const src = `export { parse, stringify as format } from "./serializer";\n`;
154
+ const result = extract(src);
155
+ expect(result).toHaveLength(2);
156
+ expect(result[0]!.name).toBe("format");
157
+ expect(result[0]!.kind).toBe("reexport");
158
+ expect(result[1]!.name).toBe("parse");
159
+ expect(result[1]!.kind).toBe("reexport");
160
+ });
161
+
162
+ it("extracts re-export `export { a, b }` (local module)", () => {
163
+ const src = `export { readFile, writeFile };\n`;
164
+ const result = extract(src);
165
+ expect(result).toHaveLength(2);
166
+ expect(result[0]!.name).toBe("readFile");
167
+ expect(result[0]!.kind).toBe("reexport");
168
+ expect(result[1]!.name).toBe("writeFile");
169
+ expect(result[1]!.kind).toBe("reexport");
170
+ });
171
+
172
+ it("extracts `export * from` as a single re-export entry", () => {
173
+ const src = `export * from "./helpers";\n`;
174
+ const result = extract(src);
175
+ expect(result).toHaveLength(1);
176
+ expect(result[0]!.name).toBe("*");
177
+ expect(result[0]!.kind).toBe("reexport");
178
+ });
179
+
180
+ // ── export default ────────────────────────────────────────────
181
+ it("extracts `export default function`", () => {
182
+ const src = `export default function createApp(opts?: Options): App;\n`;
183
+ const result = extract(src);
184
+ expect(result).toHaveLength(1);
185
+ expect(result[0]!.name).toBe("default");
186
+ expect(result[0]!.kind).toBe("function");
187
+ });
188
+
189
+ it("extracts `export default class`", () => {
190
+ const src = `export default class Logger { log(...args: unknown[]): void; }\n`;
191
+ const result = extract(src);
192
+ expect(result).toHaveLength(1);
193
+ expect(result[0]!.name).toBe("default");
194
+ expect(result[0]!.kind).toBe("class");
195
+ });
196
+
197
+ it("extracts `export default {…}` as const kind", () => {
198
+ const src = `export default { name: "app", version: "1.0" };\n`;
199
+ const result = extract(src);
200
+ expect(result).toHaveLength(1);
201
+ expect(result[0]!.name).toBe("default");
202
+ // Object literal default — kind "const" is a reasonable fallback
203
+ expect(result[0]!.kind).toBe("const");
204
+ });
205
+
206
+ // ── JSDoc / leading comments ──────────────────────────────────
207
+ it("captures leading JSDoc as doc and first line as summary", () => {
208
+ const src = [
209
+ '/** Create a typed middleware handler.',
210
+ ' * Use this to wrap Hono handlers with middleware. */',
211
+ 'export declare function createMiddleware<E>(handler: Hono): MiddlewareHandler;',
212
+ ].join("\n") + "\n";
213
+ const result = extract(src);
214
+ expect(result).toHaveLength(1);
215
+ expect(result[0]!.doc).toContain("Create a typed middleware handler");
216
+ expect(result[0]!.summary).toContain("Create a typed middleware handler");
217
+ });
218
+
219
+ it("sets summary to empty when no JSDoc", () => {
220
+ const src = `export declare function noDoc(): void;\n`;
221
+ const result = extract(src);
222
+ expect(result).toHaveLength(1);
223
+ expect(result[0]!.summary).toBe("");
224
+ });
225
+
226
+ // ── non-exported declarations are ignored ─────────────────────
227
+ it("ignores non-exported declarations", () => {
228
+ const src = `declare function internal(): void;\nfunction privateHelper(): void;\nconst secret = "hidden";\n`;
229
+ const result = extract(src);
230
+ expect(result).toHaveLength(0);
231
+ });
232
+
233
+ // ── multiple symbols in one file ──────────────────────────────
234
+ it("extracts multiple symbols from a .d.ts file", () => {
235
+ const src = [
236
+ `export interface User { id: string; }`,
237
+ `export declare function fetchUser(id: string): Promise<User>;`,
238
+ `export const MAX_RETRIES = 3;`,
239
+ `export enum Status { Active, Inactive }`,
240
+ ].join("\n");
241
+ const result = extract(src);
242
+ expect(result).toHaveLength(4);
243
+ });
244
+
245
+ // ── signature trimming ────────────────────────────────────────
246
+ it("trims trailing `{` from the signature", () => {
247
+ const src = `export interface User { id: string; name: string; }\n`;
248
+ const result = extract(src);
249
+ expect(result[0]!.signature).not.toMatch(/\{/);
250
+ });
251
+
252
+ // ── empty / no exports ────────────────────────────────────────
253
+ it("returns empty array for empty source", () => {
254
+ expect(extract("")).toHaveLength(0);
255
+ });
256
+
257
+ it("returns empty array for source with no export declarations", () => {
258
+ const src = `type Internal = string;\nconst secret = 42;\n`;
259
+ expect(extract(src)).toHaveLength(0);
260
+ });
261
+
262
+ // ── default export with JSDoc ─────────────────────────────────
263
+ it("captures JSDoc for default export", () => {
264
+ const src = `/** Main application factory. */\nexport default function createApp(opts: Options): App;\n`;
265
+ const result = extract(src);
266
+ expect(result).toHaveLength(1);
267
+ expect(result[0]!.doc).toContain("Main application factory");
268
+ });
269
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * .d.ts symbol extraction — regex/line-based, PURE (no I/O).
3
+ *
4
+ * Extracts top-level exported declarations from a `.d.ts` source string.
5
+ * Modeled on src/extract/regex.ts. Returns LibSymbol[] for a given library.
6
+ *
7
+ * Handles these export forms:
8
+ * export declare function|const|class|enum|namespace
9
+ * export function|const|class|interface|type|enum|namespace
10
+ * export abstract class
11
+ * export { a, b as c } from "..." (reexport)
12
+ * export { a, b } (local reexport)
13
+ * export * from "..." (star reexport → name: "*")
14
+ * export default … (name: "default")
15
+ *
16
+ * Non-exported declarations are ignored. Leading JSDoc is captured.
17
+ */
18
+
19
+ import type { LibSymbol, SymbolKind } from "../types.ts";
20
+ import { extractDocComment } from "../extract/docs.ts";
21
+
22
+ /**
23
+ * Extract top-level exported symbols from a .d.ts source string.
24
+ *
25
+ * @param source - Full .d.ts file content.
26
+ * @param lib - Library name.
27
+ * @param version - Installed version.
28
+ * @returns Array of extracted LibSymbol objects.
29
+ */
30
+ export function extractDtsSymbols(
31
+ source: string,
32
+ lib: string,
33
+ version: string,
34
+ ): LibSymbol[] {
35
+ const lines = source.split("\n");
36
+ const symbols: LibSymbol[] = [];
37
+ let inBlockComment = false;
38
+
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const rawLine = lines[i] as string;
41
+ const trimmed = rawLine.trim();
42
+
43
+ // Track block comments
44
+ if (inBlockComment) {
45
+ if (trimmed.includes("*/")) inBlockComment = false;
46
+ continue;
47
+ }
48
+ if (trimmed.startsWith("/*") || trimmed.startsWith("/**")) {
49
+ if (!trimmed.includes("*/")) inBlockComment = true;
50
+ continue;
51
+ }
52
+ if (trimmed.startsWith("//")) continue;
53
+ if (!trimmed) continue;
54
+
55
+ // ── Named exports: export [declare] [abstract] <kind> <name> ──
56
+ const namedExport = tryExtractNamedExport(trimmed, lines, i, lib, version, source);
57
+ if (namedExport) {
58
+ symbols.push(namedExport);
59
+ continue;
60
+ }
61
+
62
+ // ── export default … (name always "default") ──
63
+ const defaultExport = tryExtractDefaultExport(trimmed, lines, i, lib, version, source);
64
+ if (defaultExport) {
65
+ symbols.push(defaultExport);
66
+ continue;
67
+ }
68
+
69
+ // ── Re-exports: export { … } or export * from … ──
70
+ const reExport = tryExtractReexport(trimmed, lines, i, lib, version, source);
71
+ if (reExport) {
72
+ symbols.push(...reExport);
73
+ continue;
74
+ }
75
+ }
76
+
77
+ return symbols;
78
+ }
79
+
80
+ /**
81
+ * Try to extract a named export declaration (function, const, class, interface, type, enum, namespace).
82
+ */
83
+ function tryExtractNamedExport(
84
+ trimmed: string,
85
+ lines: string[],
86
+ i: number,
87
+ lib: string,
88
+ version: string,
89
+ source: string,
90
+ ): LibSymbol | null {
91
+ // Order matters: more specific before less specific.
92
+ // Each pattern: regex with named-kind capture, and a name capture group index.
93
+ const patterns: Array<{ regex: RegExp; kind: SymbolKind; nameIdx: number }> = [
94
+ // function: export [declare] [abstract] [async] function [<T>] name
95
+ { regex: /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?function\s+(?:<[^>]+>\s+)?(\w+)/, kind: "function", nameIdx: 1 },
96
+ // class: export [declare] [abstract] class name
97
+ { regex: /^export\s+(?:declare\s+)?(?:abstract\s+)?class\s+(\w+)/, kind: "class", nameIdx: 1 },
98
+ // interface: export interface name
99
+ { regex: /^export\s+interface\s+(\w+)/, kind: "interface", nameIdx: 1 },
100
+ // type: export type name [<...>] =
101
+ { regex: /^export\s+type\s+(\w+)/, kind: "type", nameIdx: 1 },
102
+ // enum: export [declare] enum name
103
+ { regex: /^export\s+(?:declare\s+)?enum\s+(\w+)/, kind: "enum", nameIdx: 1 },
104
+ // namespace: export [declare] namespace name
105
+ { regex: /^export\s+(?:declare\s+)?namespace\s+(\w+)/, kind: "namespace", nameIdx: 1 },
106
+ // const/let/var: export [declare] const name
107
+ { regex: /^export\s+(?:declare\s+)?(?:const|let|var)\s+(\w+)/, kind: "const", nameIdx: 1 },
108
+ ];
109
+
110
+ for (const p of patterns) {
111
+ const m = trimmed.match(p.regex);
112
+ if (m?.[p.nameIdx]) {
113
+ return makeSymbol(lines, i, p.kind, m[p.nameIdx]!, lib, version, source);
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Try to extract `export default …`.
122
+ * Name is always "default". Kind is determined by what follows:
123
+ * function → "function", class → "class", object/expr → "const".
124
+ */
125
+ function tryExtractDefaultExport(
126
+ trimmed: string,
127
+ lines: string[],
128
+ i: number,
129
+ lib: string,
130
+ version: string,
131
+ source: string,
132
+ ): LibSymbol | null {
133
+ if (!trimmed.startsWith("export default")) return null;
134
+
135
+ let kind: SymbolKind = "const";
136
+
137
+ if (/^export\s+default\s+(?:async\s+)?function\s/.test(trimmed)) {
138
+ kind = "function";
139
+ } else if (/^export\s+default\s+(?:abstract\s+)?class\s/.test(trimmed)) {
140
+ kind = "class";
141
+ } else if (/^export\s+default\s+interface\s/.test(trimmed)) {
142
+ kind = "interface";
143
+ }
144
+
145
+ return makeSymbol(lines, i, kind, "default", lib, version, source);
146
+ }
147
+
148
+ /**
149
+ * Try to extract re-export declarations:
150
+ * export { a, b as c } [from "..."]
151
+ * export * from "..."
152
+ */
153
+ function tryExtractReexport(
154
+ trimmed: string,
155
+ lines: string[],
156
+ i: number,
157
+ lib: string,
158
+ version: string,
159
+ source: string,
160
+ ): LibSymbol[] | null {
161
+ // export { a, b as c } [from "..."]
162
+ const braceReexport = trimmed.match(/^export\s+\{([^}]+)\}(?:\s+from\s+["'][^"']*["'])?\s*;?\s*$/);
163
+ if (braceReexport) {
164
+ const names = braceReexport[1]!.split(",").map(s => s.trim()).filter(Boolean);
165
+ return names.map((entry) => {
166
+ const aliasMatch = entry.match(/^(\w+)\s+as\s+(\w+)$/);
167
+ const name = aliasMatch?.[2] ?? entry;
168
+ return makeSymbol(lines, i, "reexport", name, lib, version, source);
169
+ });
170
+ }
171
+
172
+ // export * from "..."
173
+ if (/^export\s+\*\s+from\s+["'][^"']*["']\s*;?\s*$/.test(trimmed)) {
174
+ return [makeSymbol(lines, i, "reexport", "*", lib, version, source)];
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Build a LibSymbol from a declaration line.
182
+ * Signature is the declaration line with the body (from `{` onward) stripped.
183
+ */
184
+ function makeSymbol(
185
+ lines: string[],
186
+ lineIndex: number,
187
+ kind: SymbolKind,
188
+ name: string,
189
+ lib: string,
190
+ version: string,
191
+ source: string,
192
+ ): LibSymbol {
193
+ const rawLine = (lines[lineIndex] as string).trim();
194
+
195
+ // Signature: first line, strip everything from the first `{` onward
196
+ const sigStart = rawLine.indexOf("{");
197
+ const signature = (sigStart >= 0 ? rawLine.slice(0, sigStart) : rawLine).trim();
198
+
199
+ // Capture JSDoc
200
+ const doc = extractDocComment(source, lineIndex + 1);
201
+ const summary = (doc.split("\n")[0] || "").trim();
202
+
203
+ return {
204
+ lib,
205
+ version,
206
+ name,
207
+ kind,
208
+ signature,
209
+ doc,
210
+ summary,
211
+ card_path: "",
212
+ };
213
+ }