@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,364 @@
1
+ /**
2
+ * Init module — bootstraps Maina in a repository.
3
+ *
4
+ * Creates `.maina/` directory structure and scaffolds default files.
5
+ * Detects project type from package.json and customizes templates.
6
+ * Never overwrites existing files unless `force: true`.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { Result } from "../db/index";
12
+ import type { DetectedTool } from "../verify/detect";
13
+ import { detectTools } from "../verify/detect";
14
+
15
+ // ── Types ────────────────────────────────────────────────────────────────────
16
+
17
+ export interface InitOptions {
18
+ force?: boolean;
19
+ }
20
+
21
+ export interface InitReport {
22
+ created: string[];
23
+ skipped: string[];
24
+ directory: string;
25
+ detectedStack: DetectedStack;
26
+ detectedTools: DetectedTool[];
27
+ }
28
+
29
+ export interface DetectedStack {
30
+ runtime: "bun" | "node" | "deno" | "unknown";
31
+ language: "typescript" | "javascript" | "unknown";
32
+ testRunner: string;
33
+ linter: string;
34
+ framework: string;
35
+ }
36
+
37
+ // ── Project Detection ───────────────────────────────────────────────────────
38
+
39
+ function detectStack(repoRoot: string): DetectedStack {
40
+ const stack: DetectedStack = {
41
+ runtime: "unknown",
42
+ language: "unknown",
43
+ testRunner: "unknown",
44
+ linter: "unknown",
45
+ framework: "none",
46
+ };
47
+
48
+ // Try reading package.json
49
+ const pkgPath = join(repoRoot, "package.json");
50
+ if (!existsSync(pkgPath)) return stack;
51
+
52
+ let pkg: Record<string, unknown>;
53
+ try {
54
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
55
+ } catch {
56
+ return stack;
57
+ }
58
+
59
+ const allDeps = {
60
+ ...(pkg.dependencies as Record<string, string> | undefined),
61
+ ...(pkg.devDependencies as Record<string, string> | undefined),
62
+ ...(pkg.peerDependencies as Record<string, string> | undefined),
63
+ };
64
+
65
+ // Runtime detection
66
+ if (
67
+ allDeps["@types/bun"] ||
68
+ allDeps["bun-types"] ||
69
+ existsSync(join(repoRoot, "bun.lock"))
70
+ ) {
71
+ stack.runtime = "bun";
72
+ } else if (
73
+ existsSync(join(repoRoot, "deno.json")) ||
74
+ existsSync(join(repoRoot, "deno.jsonc"))
75
+ ) {
76
+ stack.runtime = "deno";
77
+ } else {
78
+ stack.runtime = "node";
79
+ }
80
+
81
+ // Language detection
82
+ if (existsSync(join(repoRoot, "tsconfig.json")) || allDeps.typescript) {
83
+ stack.language = "typescript";
84
+ } else {
85
+ stack.language = "javascript";
86
+ }
87
+
88
+ // Test runner detection
89
+ if (stack.runtime === "bun") {
90
+ stack.testRunner = "bun:test";
91
+ } else if (allDeps.vitest) {
92
+ stack.testRunner = "vitest";
93
+ } else if (allDeps.jest || allDeps["@jest/core"]) {
94
+ stack.testRunner = "jest";
95
+ } else if (allDeps.mocha) {
96
+ stack.testRunner = "mocha";
97
+ }
98
+
99
+ // Linter detection
100
+ if (allDeps["@biomejs/biome"]) {
101
+ stack.linter = "biome";
102
+ } else if (allDeps.eslint) {
103
+ stack.linter = "eslint";
104
+ } else if (allDeps.prettier) {
105
+ stack.linter = "prettier";
106
+ }
107
+
108
+ // Framework detection
109
+ if (allDeps.next) {
110
+ stack.framework = "next.js";
111
+ } else if (allDeps.express) {
112
+ stack.framework = "express";
113
+ } else if (allDeps.hono) {
114
+ stack.framework = "hono";
115
+ } else if (allDeps.react && !allDeps.next) {
116
+ stack.framework = "react";
117
+ } else if (allDeps.vue) {
118
+ stack.framework = "vue";
119
+ } else if (allDeps.svelte) {
120
+ stack.framework = "svelte";
121
+ }
122
+
123
+ return stack;
124
+ }
125
+
126
+ // ── Templates ────────────────────────────────────────────────────────────────
127
+
128
+ function buildConstitution(stack: DetectedStack): string {
129
+ const runtimeLine =
130
+ stack.runtime !== "unknown"
131
+ ? `- Runtime: ${stack.runtime === "bun" ? "Bun (NOT Node.js)" : stack.runtime}`
132
+ : "- Runtime: [NEEDS CLARIFICATION]";
133
+
134
+ const langLine =
135
+ stack.language !== "unknown"
136
+ ? `- Language: ${stack.language === "typescript" ? "TypeScript strict mode" : "JavaScript"}`
137
+ : "- Language: [NEEDS CLARIFICATION]";
138
+
139
+ const lintLine =
140
+ stack.linter !== "unknown"
141
+ ? `- Lint/Format: ${stack.linter}`
142
+ : "- Lint/Format: [NEEDS CLARIFICATION]";
143
+
144
+ const testLine =
145
+ stack.testRunner !== "unknown"
146
+ ? `- Test: ${stack.testRunner}`
147
+ : "- Test: [NEEDS CLARIFICATION]";
148
+
149
+ const frameworkLine =
150
+ stack.framework !== "none" ? `- Framework: ${stack.framework}\n` : "";
151
+
152
+ return `# Project Constitution
153
+
154
+ Non-negotiable rules. Injected into every AI call.
155
+
156
+ ## Stack
157
+ ${runtimeLine}
158
+ ${langLine}
159
+ ${lintLine}
160
+ ${testLine}
161
+ ${frameworkLine}
162
+ ## Architecture
163
+ - [NEEDS CLARIFICATION] Define architectural constraints.
164
+
165
+ ## Verification
166
+ - All commits pass: lint + typecheck + test
167
+ - Diff-only: only report findings on changed lines
168
+
169
+ ## Conventions
170
+ - [NEEDS CLARIFICATION] Add project-specific conventions.
171
+ `;
172
+ }
173
+
174
+ function buildAgentsMd(stack: DetectedStack): string {
175
+ const runCmd = stack.runtime === "bun" ? "bun" : "npm";
176
+ const installCmd = stack.runtime === "bun" ? "bun install" : "npm install";
177
+
178
+ return `# AGENTS.md
179
+
180
+ This repo uses [Maina](https://github.com/beeeku/maina) for verification-first development.
181
+
182
+ ## Quick Start
183
+ \`\`\`bash
184
+ ${installCmd}
185
+ maina doctor # check tool health
186
+ maina verify # run verification pipeline
187
+ maina commit # verify + commit
188
+ \`\`\`
189
+
190
+ ## Commands
191
+ | Command | Purpose |
192
+ |---------|---------|
193
+ | \`maina commit\` | Verify staged changes and commit |
194
+ | \`maina verify\` | Run full verification pipeline |
195
+ | \`maina context\` | Generate focused codebase context |
196
+ | \`maina plan <name>\` | Create feature with spec/plan/tasks |
197
+ | \`maina analyze\` | Check spec/plan consistency |
198
+ | \`maina review\` | Two-stage code review |
199
+ | \`maina stats\` | Show verification metrics |
200
+ | \`maina doctor\` | Check tool health |
201
+
202
+ ## Config Files
203
+ | File | Purpose | Who Edits |
204
+ |------|---------|-----------|
205
+ | \`.maina/constitution.md\` | Project DNA — stack, rules, gates | Team (stable, rarely changes) |
206
+ | \`AGENTS.md\` | Agent instructions — commands, conventions | Team |
207
+ | \`CLAUDE.md\` | Claude Code specific instructions | Optional, Claude Code users |
208
+ | \`.maina/prompts/*.md\` | Prompt overrides for review/commit/etc | Maina (via \`maina learn\`) |
209
+
210
+ ## Runtime
211
+ - Package manager: \`${runCmd}\`
212
+ - Test: \`${runCmd} test\`
213
+ `;
214
+ }
215
+
216
+ const REVIEW_PROMPT_TEMPLATE = `# Review Prompt
217
+
218
+ Review the following code changes for:
219
+ 1. Correctness — does the code do what it claims?
220
+ 2. Style — does it follow project conventions?
221
+ 3. Safety — are there security or performance concerns?
222
+ 4. Tests — are changes adequately tested?
223
+ `;
224
+
225
+ const COMMIT_PROMPT_TEMPLATE = `# Commit Message Prompt
226
+
227
+ Generate a conventional commit message for the staged changes.
228
+
229
+ Format: <type>(<scope>): <description>
230
+
231
+ Types: feat, fix, refactor, test, docs, chore, ci, perf
232
+ `;
233
+
234
+ function buildCiWorkflow(stack: DetectedStack): string {
235
+ const isBun = stack.runtime === "bun";
236
+ const setup = isBun
237
+ ? " - uses: oven-sh/setup-bun@v2"
238
+ : " - uses: actions/setup-node@v4";
239
+ const install = isBun ? "bun install" : "npm ci";
240
+ const check = isBun ? "bun run check" : "npm run lint";
241
+ const typecheck =
242
+ stack.language === "typescript"
243
+ ? ` - run: ${isBun ? "bun run typecheck" : "npx tsc --noEmit"}\n`
244
+ : "";
245
+ const test = isBun ? "bun test" : "npm test";
246
+
247
+ return `name: Maina CI
248
+ on: [push, pull_request]
249
+ jobs:
250
+ verify:
251
+ runs-on: ubuntu-latest
252
+ steps:
253
+ - uses: actions/checkout@v4
254
+ ${setup}
255
+ - run: ${install}
256
+ - run: ${check}
257
+ ${typecheck} - run: ${test}
258
+ `;
259
+ }
260
+
261
+ // ── File Manifest ────────────────────────────────────────────────────────────
262
+
263
+ interface FileEntry {
264
+ /** Path relative to repoRoot */
265
+ relativePath: string;
266
+ content: string;
267
+ }
268
+
269
+ function getFileManifest(stack: DetectedStack): FileEntry[] {
270
+ return [
271
+ {
272
+ relativePath: ".maina/constitution.md",
273
+ content: buildConstitution(stack),
274
+ },
275
+ {
276
+ relativePath: ".maina/prompts/review.md",
277
+ content: REVIEW_PROMPT_TEMPLATE,
278
+ },
279
+ {
280
+ relativePath: ".maina/prompts/commit.md",
281
+ content: COMMIT_PROMPT_TEMPLATE,
282
+ },
283
+ {
284
+ relativePath: "AGENTS.md",
285
+ content: buildAgentsMd(stack),
286
+ },
287
+ {
288
+ relativePath: ".github/workflows/maina-ci.yml",
289
+ content: buildCiWorkflow(stack),
290
+ },
291
+ ];
292
+ }
293
+
294
+ /** Directories to create even if they have no files */
295
+ const EXTRA_DIRS = [".maina/hooks"];
296
+
297
+ // ── Core Function ────────────────────────────────────────────────────────────
298
+
299
+ /**
300
+ * Bootstrap Maina in the given repository root.
301
+ *
302
+ * Detects project type from package.json and customizes templates.
303
+ * Creates `.maina/` directory structure and scaffolds default files.
304
+ * Never overwrites existing files unless `force: true`.
305
+ * Returns a report of what was created vs skipped.
306
+ */
307
+ export async function bootstrap(
308
+ repoRoot: string,
309
+ options?: InitOptions,
310
+ ): Promise<Result<InitReport>> {
311
+ const force = options?.force ?? false;
312
+ const mainaDir = join(repoRoot, ".maina");
313
+ const created: string[] = [];
314
+ const skipped: string[] = [];
315
+
316
+ try {
317
+ // Detect project stack from package.json
318
+ const detectedStack = detectStack(repoRoot);
319
+
320
+ // Detect available verification tools on PATH
321
+ const detectedToolsList = await detectTools();
322
+
323
+ // Ensure .maina/ exists
324
+ mkdirSync(mainaDir, { recursive: true });
325
+
326
+ // Create extra directories (e.g. hooks)
327
+ for (const dir of EXTRA_DIRS) {
328
+ mkdirSync(join(repoRoot, dir), { recursive: true });
329
+ }
330
+
331
+ // Scaffold each file with stack-aware templates
332
+ const manifest = getFileManifest(detectedStack);
333
+ for (const entry of manifest) {
334
+ const fullPath = join(repoRoot, entry.relativePath);
335
+ const dirPath = join(fullPath, "..");
336
+
337
+ // Ensure parent directory exists
338
+ mkdirSync(dirPath, { recursive: true });
339
+
340
+ if (existsSync(fullPath) && !force) {
341
+ skipped.push(entry.relativePath);
342
+ } else {
343
+ writeFileSync(fullPath, entry.content, "utf-8");
344
+ created.push(entry.relativePath);
345
+ }
346
+ }
347
+
348
+ return {
349
+ ok: true,
350
+ value: {
351
+ created,
352
+ skipped,
353
+ directory: mainaDir,
354
+ detectedStack,
355
+ detectedTools: detectedToolsList,
356
+ },
357
+ };
358
+ } catch (e) {
359
+ return {
360
+ ok: false,
361
+ error: e instanceof Error ? e.message : String(e),
362
+ };
363
+ }
364
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { detectLanguages } from "../detect";
5
+
6
+ describe("detectLanguages", () => {
7
+ const testDir = join(import.meta.dir, "__fixtures__/detect");
8
+
9
+ function setup(files: Record<string, string>) {
10
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
11
+ mkdirSync(testDir, { recursive: true });
12
+ for (const [name, content] of Object.entries(files)) {
13
+ writeFileSync(join(testDir, name), content);
14
+ }
15
+ }
16
+
17
+ function cleanup() {
18
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
19
+ }
20
+
21
+ it("should detect TypeScript from tsconfig.json", () => {
22
+ setup({ "tsconfig.json": "{}" });
23
+ const result = detectLanguages(testDir);
24
+ expect(result).toContain("typescript");
25
+ cleanup();
26
+ });
27
+
28
+ it("should detect Python from pyproject.toml", () => {
29
+ setup({ "pyproject.toml": "[tool.ruff]" });
30
+ const result = detectLanguages(testDir);
31
+ expect(result).toContain("python");
32
+ cleanup();
33
+ });
34
+
35
+ it("should detect Go from go.mod", () => {
36
+ setup({ "go.mod": "module example.com/foo" });
37
+ const result = detectLanguages(testDir);
38
+ expect(result).toContain("go");
39
+ cleanup();
40
+ });
41
+
42
+ it("should detect Rust from Cargo.toml", () => {
43
+ setup({ "Cargo.toml": '[package]\nname = "test"' });
44
+ const result = detectLanguages(testDir);
45
+ expect(result).toContain("rust");
46
+ cleanup();
47
+ });
48
+
49
+ it("should detect multiple languages in polyglot repo", () => {
50
+ setup({ "tsconfig.json": "{}", "pyproject.toml": "" });
51
+ const result = detectLanguages(testDir);
52
+ expect(result).toContain("typescript");
53
+ expect(result).toContain("python");
54
+ cleanup();
55
+ });
56
+
57
+ it("should return empty array for unknown project", () => {
58
+ setup({ "README.md": "hello" });
59
+ const result = detectLanguages(testDir);
60
+ expect(result).toHaveLength(0);
61
+ cleanup();
62
+ });
63
+
64
+ it("should detect Python from requirements.txt", () => {
65
+ setup({ "requirements.txt": "flask==2.0" });
66
+ const result = detectLanguages(testDir);
67
+ expect(result).toContain("python");
68
+ cleanup();
69
+ });
70
+
71
+ it("should detect Python from setup.py", () => {
72
+ setup({ "setup.py": "from setuptools import setup" });
73
+ const result = detectLanguages(testDir);
74
+ expect(result).toContain("python");
75
+ cleanup();
76
+ });
77
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { getProfile, TYPESCRIPT_PROFILE } from "../profile";
3
+
4
+ describe("LanguageProfile", () => {
5
+ it("should return TypeScript profile by default", () => {
6
+ const profile = getProfile("typescript");
7
+ expect(profile.id).toBe("typescript");
8
+ expect(profile.extensions).toContain(".ts");
9
+ expect(profile.extensions).toContain(".tsx");
10
+ expect(profile.syntaxTool).toBe("biome");
11
+ expect(profile.commentPrefixes).toContain("//");
12
+ });
13
+
14
+ it("should return Python profile", () => {
15
+ const profile = getProfile("python");
16
+ expect(profile.id).toBe("python");
17
+ expect(profile.extensions).toContain(".py");
18
+ expect(profile.syntaxTool).toBe("ruff");
19
+ expect(profile.commentPrefixes).toContain("#");
20
+ });
21
+
22
+ it("should return Go profile", () => {
23
+ const profile = getProfile("go");
24
+ expect(profile.id).toBe("go");
25
+ expect(profile.extensions).toContain(".go");
26
+ expect(profile.syntaxTool).toBe("go-vet");
27
+ expect(profile.commentPrefixes).toContain("//");
28
+ });
29
+
30
+ it("should return Rust profile", () => {
31
+ const profile = getProfile("rust");
32
+ expect(profile.id).toBe("rust");
33
+ expect(profile.extensions).toContain(".rs");
34
+ expect(profile.syntaxTool).toBe("clippy");
35
+ expect(profile.commentPrefixes).toContain("//");
36
+ });
37
+
38
+ it("should have test file pattern for each language", () => {
39
+ expect(TYPESCRIPT_PROFILE.testFilePattern.test("app.test.ts")).toBe(true);
40
+ expect(getProfile("python").testFilePattern.test("test_app.py")).toBe(true);
41
+ expect(getProfile("go").testFilePattern.test("app_test.go")).toBe(true);
42
+ expect(getProfile("rust").testFilePattern.test("tests/mod.rs")).toBe(true);
43
+ });
44
+
45
+ it("should have console/print patterns for slop detection", () => {
46
+ expect(TYPESCRIPT_PROFILE.printPattern.test("console.log('x')")).toBe(true);
47
+ expect(getProfile("python").printPattern.test("print('x')")).toBe(true);
48
+ expect(getProfile("go").printPattern.test("fmt.Println(x)")).toBe(true);
49
+ expect(getProfile("rust").printPattern.test("println!(x)")).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Language Detection — detects project languages from marker files.
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import type { LanguageId } from "./profile";
8
+
9
+ const LANGUAGE_MARKERS: Record<LanguageId, string[]> = {
10
+ typescript: ["tsconfig.json", "tsconfig.build.json"],
11
+ python: [
12
+ "pyproject.toml",
13
+ "setup.py",
14
+ "setup.cfg",
15
+ "requirements.txt",
16
+ "Pipfile",
17
+ ],
18
+ go: ["go.mod", "go.sum"],
19
+ rust: ["Cargo.toml", "Cargo.lock"],
20
+ };
21
+
22
+ /**
23
+ * Detect languages present in a project directory by checking for marker files.
24
+ * Returns an array of detected LanguageIds.
25
+ * Also detects TypeScript from package.json "typescript" dependency.
26
+ */
27
+ export function detectLanguages(cwd: string): LanguageId[] {
28
+ const detected: LanguageId[] = [];
29
+
30
+ for (const [lang, markers] of Object.entries(LANGUAGE_MARKERS) as [
31
+ LanguageId,
32
+ string[],
33
+ ][]) {
34
+ for (const marker of markers) {
35
+ if (existsSync(join(cwd, marker))) {
36
+ detected.push(lang);
37
+ break;
38
+ }
39
+ }
40
+ }
41
+
42
+ // Also check package.json for TypeScript dependency
43
+ if (!detected.includes("typescript")) {
44
+ const pkgPath = join(cwd, "package.json");
45
+ if (existsSync(pkgPath)) {
46
+ try {
47
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
48
+ const allDeps = {
49
+ ...pkg.dependencies,
50
+ ...pkg.devDependencies,
51
+ };
52
+ if (allDeps?.typescript) {
53
+ detected.push("typescript");
54
+ }
55
+ } catch {
56
+ // Malformed package.json — skip
57
+ }
58
+ }
59
+ }
60
+
61
+ return detected;
62
+ }
63
+
64
+ /**
65
+ * Get the primary (first detected) language, or "typescript" as fallback.
66
+ */
67
+ export function getPrimaryLanguage(cwd: string): LanguageId {
68
+ const languages = detectLanguages(cwd);
69
+ return languages[0] ?? "typescript";
70
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Language Profiles — maps each supported language to its tools,
3
+ * file patterns, and slop detection rules.
4
+ */
5
+
6
+ export type LanguageId = "typescript" | "python" | "go" | "rust";
7
+
8
+ export interface LanguageProfile {
9
+ id: LanguageId;
10
+ displayName: string;
11
+ extensions: string[];
12
+ syntaxTool: string;
13
+ syntaxArgs: (files: string[], cwd: string) => string[];
14
+ commentPrefixes: string[];
15
+ testFilePattern: RegExp;
16
+ printPattern: RegExp;
17
+ lintIgnorePattern: RegExp;
18
+ importPattern: RegExp;
19
+ fileGlobs: string[];
20
+ }
21
+
22
+ export const TYPESCRIPT_PROFILE: LanguageProfile = {
23
+ id: "typescript",
24
+ displayName: "TypeScript",
25
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
26
+ syntaxTool: "biome",
27
+ syntaxArgs: (files, _cwd) => [
28
+ "biome",
29
+ "check",
30
+ "--reporter=json",
31
+ "--no-errors-on-unmatched",
32
+ "--colors=off",
33
+ ...files,
34
+ ],
35
+ commentPrefixes: ["//", "/*"],
36
+ testFilePattern: /\.(test|spec)\.[jt]sx?$/,
37
+ printPattern: /console\.(log|warn|error|debug|info)\s*\(/,
38
+ lintIgnorePattern: /@ts-ignore|@ts-expect-error|noinspection/,
39
+ importPattern:
40
+ /^import\s+(?:type\s+)?(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["']([^"']+)["']/,
41
+ fileGlobs: ["*.ts", "*.tsx", "*.js", "*.jsx"],
42
+ };
43
+
44
+ export const PYTHON_PROFILE: LanguageProfile = {
45
+ id: "python",
46
+ displayName: "Python",
47
+ extensions: [".py", ".pyi"],
48
+ syntaxTool: "ruff",
49
+ syntaxArgs: (files, _cwd) => [
50
+ "ruff",
51
+ "check",
52
+ "--output-format=json",
53
+ ...files,
54
+ ],
55
+ commentPrefixes: ["#"],
56
+ testFilePattern: /(?:^test_|_test\.py$|tests\/)/,
57
+ printPattern: /\bprint\s*\(/,
58
+ lintIgnorePattern: /# type:\s*ignore|# noqa|# pragma:\s*no cover/,
59
+ importPattern: /^(?:from\s+(\S+)\s+import|import\s+(\S+))/,
60
+ fileGlobs: ["*.py", "*.pyi"],
61
+ };
62
+
63
+ export const GO_PROFILE: LanguageProfile = {
64
+ id: "go",
65
+ displayName: "Go",
66
+ extensions: [".go"],
67
+ syntaxTool: "go-vet",
68
+ syntaxArgs: (files, _cwd) => ["go", "vet", ...files],
69
+ commentPrefixes: ["//"],
70
+ testFilePattern: /_test\.go$/,
71
+ printPattern: /fmt\.Print(?:ln|f)?\s*\(/,
72
+ lintIgnorePattern: /\/\/\s*nolint/,
73
+ importPattern: /^\s*"([^"]+)"/,
74
+ fileGlobs: ["*.go"],
75
+ };
76
+
77
+ export const RUST_PROFILE: LanguageProfile = {
78
+ id: "rust",
79
+ displayName: "Rust",
80
+ extensions: [".rs"],
81
+ syntaxTool: "clippy",
82
+ syntaxArgs: (files, _cwd) => [
83
+ "cargo",
84
+ "clippy",
85
+ "--message-format=json",
86
+ "--",
87
+ ...files,
88
+ ],
89
+ commentPrefixes: ["//"],
90
+ testFilePattern: /(?:tests\/|_test\.rs$|#\[cfg\(test\)\])/,
91
+ printPattern: /(?:println!|print!|eprintln!|eprint!)\s*\(/,
92
+ lintIgnorePattern: /#\[allow\(|#!\[allow\(/,
93
+ importPattern: /^use\s+(\S+)/,
94
+ fileGlobs: ["*.rs"],
95
+ };
96
+
97
+ const PROFILES: Record<LanguageId, LanguageProfile> = {
98
+ typescript: TYPESCRIPT_PROFILE,
99
+ python: PYTHON_PROFILE,
100
+ go: GO_PROFILE,
101
+ rust: RUST_PROFILE,
102
+ };
103
+
104
+ export function getProfile(id: LanguageId): LanguageProfile {
105
+ return PROFILES[id];
106
+ }
107
+
108
+ export function getSupportedLanguages(): LanguageId[] {
109
+ return Object.keys(PROFILES) as LanguageId[];
110
+ }