@mainahq/core 0.2.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 (156) hide show
  1. package/README.md +31 -0
  2. package/package.json +37 -0
  3. package/src/ai/__tests__/ai.test.ts +207 -0
  4. package/src/ai/__tests__/design-approaches.test.ts +192 -0
  5. package/src/ai/__tests__/spec-questions.test.ts +191 -0
  6. package/src/ai/__tests__/tiers.test.ts +110 -0
  7. package/src/ai/commit-msg.ts +28 -0
  8. package/src/ai/design-approaches.ts +76 -0
  9. package/src/ai/index.ts +205 -0
  10. package/src/ai/pr-summary.ts +60 -0
  11. package/src/ai/spec-questions.ts +74 -0
  12. package/src/ai/tiers.ts +52 -0
  13. package/src/ai/try-generate.ts +89 -0
  14. package/src/ai/validate.ts +66 -0
  15. package/src/benchmark/__tests__/reporter.test.ts +525 -0
  16. package/src/benchmark/__tests__/runner.test.ts +113 -0
  17. package/src/benchmark/__tests__/story-loader.test.ts +152 -0
  18. package/src/benchmark/reporter.ts +332 -0
  19. package/src/benchmark/runner.ts +91 -0
  20. package/src/benchmark/story-loader.ts +88 -0
  21. package/src/benchmark/types.ts +95 -0
  22. package/src/cache/__tests__/keys.test.ts +97 -0
  23. package/src/cache/__tests__/manager.test.ts +312 -0
  24. package/src/cache/__tests__/ttl.test.ts +94 -0
  25. package/src/cache/keys.ts +44 -0
  26. package/src/cache/manager.ts +231 -0
  27. package/src/cache/ttl.ts +77 -0
  28. package/src/config/__tests__/config.test.ts +376 -0
  29. package/src/config/index.ts +198 -0
  30. package/src/context/__tests__/budget.test.ts +179 -0
  31. package/src/context/__tests__/engine.test.ts +163 -0
  32. package/src/context/__tests__/episodic.test.ts +291 -0
  33. package/src/context/__tests__/relevance.test.ts +323 -0
  34. package/src/context/__tests__/retrieval.test.ts +143 -0
  35. package/src/context/__tests__/selector.test.ts +174 -0
  36. package/src/context/__tests__/semantic.test.ts +252 -0
  37. package/src/context/__tests__/treesitter.test.ts +229 -0
  38. package/src/context/__tests__/working.test.ts +236 -0
  39. package/src/context/budget.ts +130 -0
  40. package/src/context/engine.ts +394 -0
  41. package/src/context/episodic.ts +251 -0
  42. package/src/context/relevance.ts +325 -0
  43. package/src/context/retrieval.ts +325 -0
  44. package/src/context/selector.ts +93 -0
  45. package/src/context/semantic.ts +331 -0
  46. package/src/context/treesitter.ts +216 -0
  47. package/src/context/working.ts +192 -0
  48. package/src/db/__tests__/db.test.ts +151 -0
  49. package/src/db/index.ts +211 -0
  50. package/src/db/schema.ts +84 -0
  51. package/src/design/__tests__/design.test.ts +310 -0
  52. package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
  53. package/src/design/__tests__/review.test.ts +561 -0
  54. package/src/design/index.ts +297 -0
  55. package/src/design/review.ts +327 -0
  56. package/src/explain/__tests__/explain.test.ts +173 -0
  57. package/src/explain/index.ts +181 -0
  58. package/src/features/__tests__/analyzer.test.ts +358 -0
  59. package/src/features/__tests__/checklist.test.ts +454 -0
  60. package/src/features/__tests__/numbering.test.ts +319 -0
  61. package/src/features/__tests__/quality.test.ts +295 -0
  62. package/src/features/__tests__/traceability.test.ts +147 -0
  63. package/src/features/analyzer.ts +445 -0
  64. package/src/features/checklist.ts +366 -0
  65. package/src/features/index.ts +18 -0
  66. package/src/features/numbering.ts +404 -0
  67. package/src/features/quality.ts +349 -0
  68. package/src/features/test-stubs.ts +157 -0
  69. package/src/features/traceability.ts +260 -0
  70. package/src/feedback/__tests__/async-feedback.test.ts +52 -0
  71. package/src/feedback/__tests__/collector.test.ts +219 -0
  72. package/src/feedback/__tests__/compress.test.ts +150 -0
  73. package/src/feedback/__tests__/preferences.test.ts +169 -0
  74. package/src/feedback/collector.ts +135 -0
  75. package/src/feedback/compress.ts +92 -0
  76. package/src/feedback/preferences.ts +108 -0
  77. package/src/git/__tests__/git.test.ts +62 -0
  78. package/src/git/index.ts +110 -0
  79. package/src/hooks/__tests__/runner.test.ts +266 -0
  80. package/src/hooks/index.ts +8 -0
  81. package/src/hooks/runner.ts +130 -0
  82. package/src/index.ts +356 -0
  83. package/src/init/__tests__/init.test.ts +228 -0
  84. package/src/init/index.ts +364 -0
  85. package/src/language/__tests__/detect.test.ts +77 -0
  86. package/src/language/__tests__/profile.test.ts +51 -0
  87. package/src/language/detect.ts +70 -0
  88. package/src/language/profile.ts +110 -0
  89. package/src/prompts/__tests__/defaults.test.ts +52 -0
  90. package/src/prompts/__tests__/engine.test.ts +183 -0
  91. package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
  92. package/src/prompts/__tests__/evolution.test.ts +187 -0
  93. package/src/prompts/__tests__/loader.test.ts +105 -0
  94. package/src/prompts/candidates/review-v2.md +55 -0
  95. package/src/prompts/defaults/ai-review.md +49 -0
  96. package/src/prompts/defaults/commit.md +30 -0
  97. package/src/prompts/defaults/context.md +26 -0
  98. package/src/prompts/defaults/design-approaches.md +57 -0
  99. package/src/prompts/defaults/design-hld-lld.md +55 -0
  100. package/src/prompts/defaults/design.md +53 -0
  101. package/src/prompts/defaults/explain.md +31 -0
  102. package/src/prompts/defaults/fix.md +32 -0
  103. package/src/prompts/defaults/index.ts +38 -0
  104. package/src/prompts/defaults/review.md +41 -0
  105. package/src/prompts/defaults/spec-questions.md +59 -0
  106. package/src/prompts/defaults/tests.md +72 -0
  107. package/src/prompts/engine.ts +137 -0
  108. package/src/prompts/evolution.ts +409 -0
  109. package/src/prompts/loader.ts +71 -0
  110. package/src/review/__tests__/review.test.ts +288 -0
  111. package/src/review/comprehensive.ts +362 -0
  112. package/src/review/index.ts +417 -0
  113. package/src/stats/__tests__/tracker.test.ts +323 -0
  114. package/src/stats/index.ts +11 -0
  115. package/src/stats/tracker.ts +492 -0
  116. package/src/ticket/__tests__/ticket.test.ts +273 -0
  117. package/src/ticket/index.ts +185 -0
  118. package/src/utils.ts +87 -0
  119. package/src/verify/__tests__/ai-review.test.ts +242 -0
  120. package/src/verify/__tests__/coverage.test.ts +83 -0
  121. package/src/verify/__tests__/detect.test.ts +175 -0
  122. package/src/verify/__tests__/diff-filter.test.ts +338 -0
  123. package/src/verify/__tests__/fix.test.ts +478 -0
  124. package/src/verify/__tests__/linters/clippy.test.ts +45 -0
  125. package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
  126. package/src/verify/__tests__/linters/ruff.test.ts +64 -0
  127. package/src/verify/__tests__/mutation.test.ts +141 -0
  128. package/src/verify/__tests__/pipeline.test.ts +553 -0
  129. package/src/verify/__tests__/proof.test.ts +97 -0
  130. package/src/verify/__tests__/secretlint.test.ts +190 -0
  131. package/src/verify/__tests__/semgrep.test.ts +217 -0
  132. package/src/verify/__tests__/slop.test.ts +366 -0
  133. package/src/verify/__tests__/sonar.test.ts +113 -0
  134. package/src/verify/__tests__/syntax-guard.test.ts +227 -0
  135. package/src/verify/__tests__/trivy.test.ts +191 -0
  136. package/src/verify/__tests__/visual.test.ts +139 -0
  137. package/src/verify/ai-review.ts +276 -0
  138. package/src/verify/coverage.ts +134 -0
  139. package/src/verify/detect.ts +171 -0
  140. package/src/verify/diff-filter.ts +183 -0
  141. package/src/verify/fix.ts +317 -0
  142. package/src/verify/linters/clippy.ts +52 -0
  143. package/src/verify/linters/go-vet.ts +32 -0
  144. package/src/verify/linters/ruff.ts +47 -0
  145. package/src/verify/mutation.ts +143 -0
  146. package/src/verify/pipeline.ts +328 -0
  147. package/src/verify/proof.ts +277 -0
  148. package/src/verify/secretlint.ts +168 -0
  149. package/src/verify/semgrep.ts +170 -0
  150. package/src/verify/slop.ts +493 -0
  151. package/src/verify/sonar.ts +146 -0
  152. package/src/verify/syntax-guard.ts +251 -0
  153. package/src/verify/trivy.ts +161 -0
  154. package/src/verify/visual.ts +460 -0
  155. package/src/workflow/__tests__/context.test.ts +110 -0
  156. package/src/workflow/context.ts +81 -0
@@ -0,0 +1,93 @@
1
+ import type { BudgetMode } from "./budget.ts";
2
+
3
+ export type MainaCommand =
4
+ | "commit"
5
+ | "verify"
6
+ | "context"
7
+ | "review"
8
+ | "plan"
9
+ | "explain"
10
+ | "design"
11
+ | "ticket"
12
+ | "analyze"
13
+ | "pr";
14
+
15
+ export interface ContextNeeds {
16
+ working: boolean;
17
+ episodic: boolean | string[];
18
+ semantic: boolean | string[];
19
+ retrieval: boolean;
20
+ }
21
+
22
+ const CONTEXT_NEEDS: Record<MainaCommand, ContextNeeds> = {
23
+ commit: {
24
+ working: true,
25
+ episodic: false,
26
+ semantic: ["conventions"],
27
+ retrieval: false,
28
+ },
29
+ verify: {
30
+ working: true,
31
+ episodic: ["recent-reviews"],
32
+ semantic: ["adrs", "conventions"],
33
+ retrieval: false,
34
+ },
35
+ context: { working: true, episodic: true, semantic: true, retrieval: true },
36
+ review: {
37
+ working: true,
38
+ episodic: ["past-reviews"],
39
+ semantic: ["adrs"],
40
+ retrieval: false,
41
+ },
42
+ plan: {
43
+ working: true,
44
+ semantic: ["adrs", "conventions"],
45
+ episodic: false,
46
+ retrieval: false,
47
+ },
48
+ explain: { working: true, episodic: false, semantic: true, retrieval: true },
49
+ design: {
50
+ working: true,
51
+ episodic: false,
52
+ semantic: ["adrs"],
53
+ retrieval: false,
54
+ },
55
+ ticket: {
56
+ working: false,
57
+ episodic: false,
58
+ semantic: ["modules"],
59
+ retrieval: false,
60
+ },
61
+ analyze: { working: true, episodic: true, semantic: true, retrieval: false },
62
+ pr: {
63
+ working: true,
64
+ episodic: ["past-reviews"],
65
+ semantic: true,
66
+ retrieval: true,
67
+ },
68
+ };
69
+
70
+ export function getContextNeeds(command: MainaCommand): ContextNeeds {
71
+ return CONTEXT_NEEDS[command];
72
+ }
73
+
74
+ export function needsLayer(
75
+ needs: ContextNeeds,
76
+ layer: "working" | "episodic" | "semantic" | "retrieval",
77
+ ): boolean {
78
+ const value = needs[layer];
79
+ if (Array.isArray(value)) {
80
+ return value.length > 0;
81
+ }
82
+ return value === true;
83
+ }
84
+
85
+ export function getBudgetMode(command: MainaCommand): BudgetMode {
86
+ if (command === "commit") {
87
+ return "focused";
88
+ }
89
+ if (command === "context" || command === "explain") {
90
+ return "explore";
91
+ }
92
+ return "default";
93
+ }
@@ -0,0 +1,331 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { getContextDb } from "../db/index";
4
+ import {
5
+ buildGraph,
6
+ type DependencyGraph,
7
+ scoreRelevance,
8
+ type TaskContext,
9
+ } from "./relevance";
10
+ import { parseFile } from "./treesitter";
11
+
12
+ export interface SemanticContext {
13
+ entities: {
14
+ filePath: string;
15
+ name: string;
16
+ kind: string;
17
+ relevance: number;
18
+ }[];
19
+ graph: DependencyGraph;
20
+ scores: Map<string, number>;
21
+ constitution: string | null;
22
+ customContext: string[];
23
+ }
24
+
25
+ const SOURCE_EXTENSIONS = new Set([
26
+ ".ts",
27
+ ".tsx",
28
+ ".js",
29
+ ".jsx",
30
+ ".py",
31
+ ".pyi",
32
+ ".go",
33
+ ".rs",
34
+ ]);
35
+
36
+ /**
37
+ * Recursively walks a directory and collects source files,
38
+ * excluding node_modules, dist, and .git directories.
39
+ */
40
+ function collectSourceFiles(dir: string): string[] {
41
+ const results: string[] = [];
42
+ const SKIP_DIRS = new Set(["node_modules", "dist", ".git"]);
43
+
44
+ function walk(current: string): void {
45
+ let entries: string[];
46
+ try {
47
+ entries = readdirSync(current) as unknown as string[];
48
+ } catch {
49
+ return;
50
+ }
51
+
52
+ for (const entry of entries) {
53
+ if (SKIP_DIRS.has(entry)) continue;
54
+
55
+ const fullPath = join(current, entry);
56
+ let stat: ReturnType<typeof statSync> | undefined;
57
+ try {
58
+ stat = statSync(fullPath);
59
+ } catch {
60
+ continue;
61
+ }
62
+
63
+ if (stat.isDirectory()) {
64
+ walk(fullPath);
65
+ } else if (stat.isFile()) {
66
+ const dotIdx = entry.lastIndexOf(".");
67
+ const ext = dotIdx >= 0 ? entry.slice(dotIdx) : "";
68
+ if (SOURCE_EXTENSIONS.has(ext)) {
69
+ results.push(fullPath);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ walk(dir);
76
+ return results;
77
+ }
78
+
79
+ /**
80
+ * Reads .maina/constitution.md if it exists, returning its content.
81
+ * Returns null if the file does not exist or cannot be read.
82
+ */
83
+ export async function loadConstitution(
84
+ mainaDir: string,
85
+ ): Promise<string | null> {
86
+ const constitutionPath = join(mainaDir, "constitution.md");
87
+ try {
88
+ const exists = await Bun.file(constitutionPath).exists();
89
+ if (!exists) return null;
90
+ return await Bun.file(constitutionPath).text();
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Reads all files from .maina/context/semantic/custom/ and returns their contents.
98
+ * Returns an empty array if the directory doesn't exist or is empty.
99
+ */
100
+ export async function loadCustomContext(mainaDir: string): Promise<string[]> {
101
+ const customDir = join(mainaDir, "context", "semantic", "custom");
102
+
103
+ let entries: string[];
104
+ try {
105
+ entries = readdirSync(customDir) as unknown as string[];
106
+ } catch {
107
+ return [];
108
+ }
109
+
110
+ const results: string[] = [];
111
+ for (const entry of entries) {
112
+ const filePath = join(customDir, entry);
113
+ try {
114
+ const stat = statSync(filePath);
115
+ if (!stat.isFile()) continue;
116
+ const content = await Bun.file(filePath).text();
117
+ results.push(content);
118
+ } catch {}
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ /**
125
+ * Builds the full semantic context for a repo:
126
+ * - Scans .ts/.js files (excluding node_modules/dist/.git)
127
+ * - Builds dependency graph
128
+ * - Runs PageRank (personalized toward task context)
129
+ * - Loads constitution and custom context
130
+ * - Attaches relevance scores to all parsed entities
131
+ */
132
+ export async function buildSemanticContext(
133
+ repoRoot: string,
134
+ mainaDir: string,
135
+ taskContext: TaskContext,
136
+ ): Promise<SemanticContext> {
137
+ // Collect all relevant source files
138
+ const files = collectSourceFiles(repoRoot);
139
+
140
+ // Build dependency graph
141
+ const graph = await buildGraph(files);
142
+
143
+ // Run PageRank with task personalization
144
+ const scores = scoreRelevance(graph, taskContext);
145
+
146
+ // Parse entities from all files and annotate with relevance.
147
+ // Entity filePaths are stored as relative to repoRoot for LLM consumption.
148
+ const entities: SemanticContext["entities"] = [];
149
+
150
+ for (const file of files) {
151
+ const fileScore = scores.get(file) ?? 0;
152
+ const relPath = relative(repoRoot, file);
153
+ let parsed: Awaited<ReturnType<typeof parseFile>> | undefined;
154
+ try {
155
+ parsed = await parseFile(file);
156
+ } catch {
157
+ continue;
158
+ }
159
+
160
+ for (const entity of parsed.entities) {
161
+ entities.push({
162
+ filePath: relPath,
163
+ name: entity.name,
164
+ kind: entity.kind,
165
+ relevance: fileScore,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Load constitution and custom context
171
+ const [constitution, customContext] = await Promise.all([
172
+ loadConstitution(mainaDir),
173
+ loadCustomContext(mainaDir),
174
+ ]);
175
+
176
+ return {
177
+ entities,
178
+ graph,
179
+ scores,
180
+ constitution,
181
+ customContext,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Returns top N entities sorted by relevance score (descending).
187
+ * Default N is 20.
188
+ */
189
+ export function getTopEntities(
190
+ context: SemanticContext,
191
+ n = 20,
192
+ ): { filePath: string; name: string; kind: string; relevance: number }[] {
193
+ return [...context.entities]
194
+ .sort((a, b) => b.relevance - a.relevance)
195
+ .slice(0, n);
196
+ }
197
+
198
+ /**
199
+ * Formats the semantic context for LLM consumption.
200
+ * If filter is provided, only includes sections matching the filter terms.
201
+ */
202
+ export function assembleSemanticText(
203
+ context: SemanticContext,
204
+ filter?: string[],
205
+ ): string {
206
+ const parts: string[] = [];
207
+
208
+ const shouldInclude = (sectionName: string): boolean => {
209
+ if (!filter || filter.length === 0) return true;
210
+ const lower = sectionName.toLowerCase();
211
+ return filter.some((f) => lower.includes(f.toLowerCase()));
212
+ };
213
+
214
+ // Constitution
215
+ if (context.constitution && shouldInclude("constitution")) {
216
+ parts.push("## Project Constitution\n");
217
+ parts.push(context.constitution);
218
+ }
219
+
220
+ // Custom context files
221
+ if (context.customContext.length > 0) {
222
+ for (const content of context.customContext) {
223
+ // Determine section name from content heading (first line)
224
+ const firstLine = content.split("\n")[0] ?? "";
225
+ const sectionName = firstLine.replace(/^#+\s*/, "").trim() || "custom";
226
+
227
+ if (shouldInclude(sectionName) || shouldInclude("custom")) {
228
+ parts.push(content);
229
+ }
230
+ }
231
+ }
232
+
233
+ // Codebase overview section: group entities by file, sorted by relevance
234
+ if (shouldInclude("entities") || !filter || filter.length === 0) {
235
+ const topEntities = getTopEntities(context, 200);
236
+ if (topEntities.length > 0) {
237
+ // Group entities by filePath
238
+ const byFile = new Map<
239
+ string,
240
+ { kind: string; name: string; relevance: number }[]
241
+ >();
242
+ for (const entity of topEntities) {
243
+ const existing = byFile.get(entity.filePath) ?? [];
244
+ existing.push({
245
+ kind: entity.kind,
246
+ name: entity.name,
247
+ relevance: entity.relevance,
248
+ });
249
+ byFile.set(entity.filePath, existing);
250
+ }
251
+
252
+ // Sort files by max relevance descending
253
+ const sortedFiles = [...byFile.entries()].sort((a, b) => {
254
+ const maxA = Math.max(...a[1].map((e) => e.relevance));
255
+ const maxB = Math.max(...b[1].map((e) => e.relevance));
256
+ return maxB - maxA;
257
+ });
258
+
259
+ parts.push("## Codebase Overview\n");
260
+ for (const [filePath, entities] of sortedFiles) {
261
+ const fileScore = Math.max(...entities.map((e) => e.relevance));
262
+ parts.push(`### ${filePath} (relevance: ${fileScore.toFixed(4)})`);
263
+ for (const entity of entities) {
264
+ parts.push(`- \`${entity.name}\` (${entity.kind})`);
265
+ }
266
+ parts.push("");
267
+ }
268
+ }
269
+ }
270
+
271
+ return parts.join("\n");
272
+ }
273
+
274
+ /**
275
+ * Persist semantic context (entities + dependency edges) to the context DB.
276
+ * Replaces all existing data — this is a full re-index.
277
+ * Never throws — silently fails on DB errors.
278
+ */
279
+ export function persistSemanticContext(
280
+ mainaDir: string,
281
+ context: SemanticContext,
282
+ repoRoot: string,
283
+ ): void {
284
+ try {
285
+ const dbResult = getContextDb(mainaDir);
286
+ if (!dbResult.ok) return;
287
+
288
+ const db = dbResult.value.db;
289
+ const now = new Date().toISOString();
290
+
291
+ // Clear existing data and re-insert (full re-index)
292
+ db.exec("DELETE FROM semantic_entities");
293
+ db.exec("DELETE FROM dependency_edges");
294
+
295
+ // Insert entities
296
+ const insertEntity = db.prepare(
297
+ `INSERT INTO semantic_entities (id, file_path, name, kind, start_line, end_line, updated_at)
298
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
299
+ );
300
+ for (const entity of context.entities) {
301
+ insertEntity.run(
302
+ crypto.randomUUID(),
303
+ entity.filePath,
304
+ entity.name,
305
+ entity.kind,
306
+ 0,
307
+ 0,
308
+ now,
309
+ );
310
+ }
311
+
312
+ // Insert dependency edges
313
+ const insertEdge = db.prepare(
314
+ `INSERT INTO dependency_edges (id, source_file, target_file, weight, type)
315
+ VALUES (?, ?, ?, ?, ?)`,
316
+ );
317
+ for (const [source, targets] of context.graph.edges) {
318
+ for (const [target, weight] of targets) {
319
+ insertEdge.run(
320
+ crypto.randomUUID(),
321
+ relative(repoRoot, source),
322
+ relative(repoRoot, target),
323
+ weight,
324
+ "import",
325
+ );
326
+ }
327
+ }
328
+ } catch {
329
+ // Persistence failure should never propagate
330
+ }
331
+ }
@@ -0,0 +1,216 @@
1
+ export interface ParsedEntity {
2
+ name: string;
3
+ kind: "function" | "class" | "interface" | "type" | "variable";
4
+ startLine: number;
5
+ endLine: number;
6
+ }
7
+
8
+ export interface ParsedImport {
9
+ source: string;
10
+ specifiers: string[];
11
+ isDefault: boolean;
12
+ }
13
+
14
+ export interface ParsedExport {
15
+ name: string;
16
+ kind: string;
17
+ }
18
+
19
+ export interface ParseResult {
20
+ imports: ParsedImport[];
21
+ exports: ParsedExport[];
22
+ entities: ParsedEntity[];
23
+ }
24
+
25
+ /**
26
+ * Extracts import statements from TypeScript/JavaScript source content.
27
+ * Handles named imports, default imports, namespace imports, and type imports.
28
+ * Results are returned in source-order (line order).
29
+ */
30
+ export function extractImports(content: string): ParsedImport[] {
31
+ // Collect all matches with their index so we can sort by position
32
+ type RawMatch = { index: number; result: ParsedImport };
33
+ const raw: RawMatch[] = [];
34
+
35
+ // Match: import * as X from "..."
36
+ const namespaceRe = /^import\s+\*\s+as\s+(\w+)\s+from\s+["']([^"']+)["']/gm;
37
+ for (const match of content.matchAll(namespaceRe)) {
38
+ const name = match[1];
39
+ const source = match[2];
40
+ if (name && source) {
41
+ raw.push({
42
+ index: match.index ?? 0,
43
+ result: { source, specifiers: [name], isDefault: false },
44
+ });
45
+ }
46
+ }
47
+
48
+ // Match: import { X, Y } from "..." (including import type { ... })
49
+ const namedRe =
50
+ /^import\s+(?:type\s+)?\{\s*([^}]+)\}\s+from\s+["']([^"']+)["']/gm;
51
+ for (const match of content.matchAll(namedRe)) {
52
+ const specifierStr = match[1];
53
+ const source = match[2];
54
+ if (specifierStr && source) {
55
+ const specifiers = specifierStr
56
+ .split(",")
57
+ .map((s) => s.trim())
58
+ .filter((s) => s.length > 0);
59
+ raw.push({
60
+ index: match.index ?? 0,
61
+ result: { source, specifiers, isDefault: false },
62
+ });
63
+ }
64
+ }
65
+
66
+ // Match: import X from "..." (default import, but NOT namespace or named)
67
+ const defaultRe = /^import\s+(\w+)\s+from\s+["']([^"']+)["']/gm;
68
+ for (const match of content.matchAll(defaultRe)) {
69
+ const name = match[1];
70
+ const source = match[2];
71
+ if (name && source) {
72
+ // Avoid double-counting "type" keyword (import type { ... } matched above)
73
+ if (name === "type") continue;
74
+ raw.push({
75
+ index: match.index ?? 0,
76
+ result: { source, specifiers: [name], isDefault: true },
77
+ });
78
+ }
79
+ }
80
+
81
+ // Sort by position in source file and return
82
+ raw.sort((a, b) => a.index - b.index);
83
+ return raw.map((r) => r.result);
84
+ }
85
+
86
+ /**
87
+ * Extracts export declarations from TypeScript/JavaScript source content.
88
+ * Handles exported functions, classes, interfaces, types, consts, defaults, and re-exports.
89
+ */
90
+ export function extractExports(content: string): ParsedExport[] {
91
+ const results: ParsedExport[] = [];
92
+
93
+ // export function / export async function
94
+ const funcRe = /^export\s+(?:async\s+)?function\s+(\w+)/gm;
95
+ for (const match of content.matchAll(funcRe)) {
96
+ const name = match[1];
97
+ if (name) results.push({ name, kind: "function" });
98
+ }
99
+
100
+ // export class
101
+ const classRe = /^export\s+class\s+(\w+)/gm;
102
+ for (const match of content.matchAll(classRe)) {
103
+ const name = match[1];
104
+ if (name) results.push({ name, kind: "class" });
105
+ }
106
+
107
+ // export interface
108
+ const interfaceRe = /^export\s+interface\s+(\w+)/gm;
109
+ for (const match of content.matchAll(interfaceRe)) {
110
+ const name = match[1];
111
+ if (name) results.push({ name, kind: "interface" });
112
+ }
113
+
114
+ // export type X =
115
+ const typeRe = /^export\s+type\s+(\w+)\s*=/gm;
116
+ for (const match of content.matchAll(typeRe)) {
117
+ const name = match[1];
118
+ if (name) results.push({ name, kind: "type" });
119
+ }
120
+
121
+ // export const / export let / export var
122
+ const constRe = /^export\s+(?:const|let|var)\s+(\w+)/gm;
123
+ for (const match of content.matchAll(constRe)) {
124
+ const name = match[1];
125
+ if (name) results.push({ name, kind: "variable" });
126
+ }
127
+
128
+ // export default (class/function/expression)
129
+ const defaultClassRe = /^export\s+default\s+(?:class|function)\s+(\w+)/gm;
130
+ for (const _match of content.matchAll(defaultClassRe)) {
131
+ // Named default export — still record as "default"
132
+ results.push({ name: "default", kind: "default" });
133
+ }
134
+ // export default (bare keyword, no named class/function)
135
+ const defaultBareRe = /^export\s+default\s+(?!class\s+\w|function\s+\w)/gm;
136
+ for (const _match of content.matchAll(defaultBareRe)) {
137
+ results.push({ name: "default", kind: "default" });
138
+ }
139
+
140
+ // export { X, Y } or export { X, Y } from "..."
141
+ const reExportRe = /^export\s+\{\s*([^}]+)\}/gm;
142
+ for (const match of content.matchAll(reExportRe)) {
143
+ const specifierStr = match[1];
144
+ if (specifierStr) {
145
+ const specifiers = specifierStr
146
+ .split(",")
147
+ .map((s) => {
148
+ // handle "X as Y" — use the exported name (Y)
149
+ const parts = s.trim().split(/\s+as\s+/);
150
+ return (parts[parts.length - 1] ?? "").trim();
151
+ })
152
+ .filter((s) => s.length > 0);
153
+ for (const name of specifiers) {
154
+ results.push({ name, kind: "reexport" });
155
+ }
156
+ }
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * Extracts top-level entity declarations (functions, classes, interfaces, types, variables)
164
+ * with their starting line numbers.
165
+ */
166
+ export function extractEntities(content: string): ParsedEntity[] {
167
+ const results: ParsedEntity[] = [];
168
+ const lines = content.split("\n");
169
+
170
+ // Patterns: each tuple is [regex, kind]
171
+ const patterns: Array<[RegExp, ParsedEntity["kind"]]> = [
172
+ // function declarations (exported or not, async or not)
173
+ [/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/, "function"],
174
+ // class declarations
175
+ [/^(?:export\s+)?class\s+(\w+)/, "class"],
176
+ // interface declarations
177
+ [/^(?:export\s+)?interface\s+(\w+)/, "interface"],
178
+ // type alias: type X =
179
+ [/^(?:export\s+)?type\s+(\w+)\s*=/, "type"],
180
+ // top-level const/let/var (must start at column 0)
181
+ [/^(?:export\s+)?(?:const|let|var)\s+(\w+)/, "variable"],
182
+ ];
183
+
184
+ for (let i = 0; i < lines.length; i++) {
185
+ const line = lines[i] ?? "";
186
+ const lineNum = i + 1; // 1-based
187
+
188
+ for (const [pattern, kind] of patterns) {
189
+ const match = line.match(pattern);
190
+ if (match) {
191
+ const name = match[1];
192
+ if (name) {
193
+ results.push({ name, kind, startLine: lineNum, endLine: lineNum });
194
+ break; // only match one pattern per line
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ return results;
201
+ }
202
+
203
+ /**
204
+ * Reads a TypeScript/JavaScript file from disk and returns its parsed structure:
205
+ * imports, exports, and top-level entities with line numbers.
206
+ */
207
+ export async function parseFile(filePath: string): Promise<ParseResult> {
208
+ const file = Bun.file(filePath);
209
+ const content = await file.text();
210
+
211
+ return {
212
+ imports: extractImports(content),
213
+ exports: extractExports(content),
214
+ entities: extractEntities(content),
215
+ };
216
+ }