@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,319 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { DesignChoices } from "../numbering";
6
+ import {
7
+ createFeatureDir,
8
+ getNextFeatureNumber,
9
+ scaffoldFeature,
10
+ scaffoldFeatureWithContext,
11
+ } from "../numbering";
12
+
13
+ function makeTmpDir(): string {
14
+ const dir = join(
15
+ tmpdir(),
16
+ `maina-features-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
17
+ );
18
+ mkdirSync(dir, { recursive: true });
19
+ return dir;
20
+ }
21
+
22
+ describe("getNextFeatureNumber", () => {
23
+ let tmpDir: string;
24
+
25
+ beforeEach(() => {
26
+ tmpDir = makeTmpDir();
27
+ });
28
+
29
+ afterEach(() => {
30
+ rmSync(tmpDir, { recursive: true, force: true });
31
+ });
32
+
33
+ test("empty dir returns '001'", async () => {
34
+ const featuresDir = join(tmpDir, ".maina", "features");
35
+ mkdirSync(featuresDir, { recursive: true });
36
+ const result = await getNextFeatureNumber(tmpDir);
37
+ expect(result.ok).toBe(true);
38
+ if (result.ok) {
39
+ expect(result.value).toBe("001");
40
+ }
41
+ });
42
+
43
+ test("existing 001, 002 returns '003'", async () => {
44
+ const featuresDir = join(tmpDir, ".maina", "features");
45
+ mkdirSync(join(featuresDir, "001-first-feature"), { recursive: true });
46
+ mkdirSync(join(featuresDir, "002-second-feature"), { recursive: true });
47
+ const result = await getNextFeatureNumber(tmpDir);
48
+ expect(result.ok).toBe(true);
49
+ if (result.ok) {
50
+ expect(result.value).toBe("003");
51
+ }
52
+ });
53
+
54
+ test("non-sequential (001, 003) returns '004'", async () => {
55
+ const featuresDir = join(tmpDir, ".maina", "features");
56
+ mkdirSync(join(featuresDir, "001-first"), { recursive: true });
57
+ mkdirSync(join(featuresDir, "003-third"), { recursive: true });
58
+ const result = await getNextFeatureNumber(tmpDir);
59
+ expect(result.ok).toBe(true);
60
+ if (result.ok) {
61
+ expect(result.value).toBe("004");
62
+ }
63
+ });
64
+
65
+ test("no .maina/features dir yet creates it and returns '001'", async () => {
66
+ // tmpDir exists but has no .maina/features
67
+ const result = await getNextFeatureNumber(tmpDir);
68
+ expect(result.ok).toBe(true);
69
+ if (result.ok) {
70
+ expect(result.value).toBe("001");
71
+ }
72
+ expect(existsSync(join(tmpDir, ".maina", "features"))).toBe(true);
73
+ });
74
+
75
+ test("ignores non-directory entries", async () => {
76
+ const featuresDir = join(tmpDir, ".maina", "features");
77
+ mkdirSync(featuresDir, { recursive: true });
78
+ mkdirSync(join(featuresDir, "001-first"), { recursive: true });
79
+ // Create a file that looks like a feature but isn't a directory
80
+ Bun.write(join(featuresDir, "002-not-a-dir.md"), "just a file");
81
+ const result = await getNextFeatureNumber(tmpDir);
82
+ expect(result.ok).toBe(true);
83
+ if (result.ok) {
84
+ expect(result.value).toBe("002");
85
+ }
86
+ });
87
+
88
+ test("ignores directories without numeric prefix", async () => {
89
+ const featuresDir = join(tmpDir, ".maina", "features");
90
+ mkdirSync(featuresDir, { recursive: true });
91
+ mkdirSync(join(featuresDir, "001-first"), { recursive: true });
92
+ mkdirSync(join(featuresDir, "not-numbered"), { recursive: true });
93
+ const result = await getNextFeatureNumber(tmpDir);
94
+ expect(result.ok).toBe(true);
95
+ if (result.ok) {
96
+ expect(result.value).toBe("002");
97
+ }
98
+ });
99
+ });
100
+
101
+ describe("createFeatureDir", () => {
102
+ let tmpDir: string;
103
+
104
+ beforeEach(() => {
105
+ tmpDir = makeTmpDir();
106
+ mkdirSync(join(tmpDir, ".maina", "features"), { recursive: true });
107
+ });
108
+
109
+ afterEach(() => {
110
+ rmSync(tmpDir, { recursive: true, force: true });
111
+ });
112
+
113
+ test("creates correct directory structure", async () => {
114
+ const result = await createFeatureDir(tmpDir, "001", "my-feature");
115
+ expect(result.ok).toBe(true);
116
+ if (result.ok) {
117
+ expect(existsSync(result.value)).toBe(true);
118
+ }
119
+ });
120
+
121
+ test("returns full path", async () => {
122
+ const result = await createFeatureDir(tmpDir, "001", "my-feature");
123
+ expect(result.ok).toBe(true);
124
+ if (result.ok) {
125
+ const expected = join(tmpDir, ".maina", "features", "001-my-feature");
126
+ expect(result.value).toBe(expected);
127
+ }
128
+ });
129
+
130
+ test("handles kebab-case conversion from spaces", async () => {
131
+ const result = await createFeatureDir(tmpDir, "002", "My Cool Feature");
132
+ expect(result.ok).toBe(true);
133
+ if (result.ok) {
134
+ expect(result.value).toContain("002-my-cool-feature");
135
+ expect(existsSync(result.value)).toBe(true);
136
+ }
137
+ });
138
+
139
+ test("handles kebab-case conversion from camelCase", async () => {
140
+ const result = await createFeatureDir(tmpDir, "003", "myCoolFeature");
141
+ expect(result.ok).toBe(true);
142
+ if (result.ok) {
143
+ expect(result.value).toContain("003-my-cool-feature");
144
+ expect(existsSync(result.value)).toBe(true);
145
+ }
146
+ });
147
+
148
+ test("handles kebab-case conversion from PascalCase", async () => {
149
+ const result = await createFeatureDir(tmpDir, "004", "MyCoolFeature");
150
+ expect(result.ok).toBe(true);
151
+ if (result.ok) {
152
+ expect(result.value).toContain("004-my-cool-feature");
153
+ expect(existsSync(result.value)).toBe(true);
154
+ }
155
+ });
156
+
157
+ test("returns error if directory already exists", async () => {
158
+ const dir = join(tmpDir, ".maina", "features", "001-my-feature");
159
+ mkdirSync(dir, { recursive: true });
160
+ const result = await createFeatureDir(tmpDir, "001", "my-feature");
161
+ expect(result.ok).toBe(false);
162
+ if (!result.ok) {
163
+ expect(result.error).toContain("already exists");
164
+ }
165
+ });
166
+ });
167
+
168
+ describe("scaffoldFeature", () => {
169
+ let tmpDir: string;
170
+ let featureDir: string;
171
+
172
+ beforeEach(() => {
173
+ tmpDir = makeTmpDir();
174
+ featureDir = join(tmpDir, ".maina", "features", "001-test-feature");
175
+ mkdirSync(featureDir, { recursive: true });
176
+ });
177
+
178
+ afterEach(() => {
179
+ rmSync(tmpDir, { recursive: true, force: true });
180
+ });
181
+
182
+ test("creates spec.md, plan.md, tasks.md", async () => {
183
+ const result = await scaffoldFeature(featureDir);
184
+ expect(result.ok).toBe(true);
185
+ expect(existsSync(join(featureDir, "spec.md"))).toBe(true);
186
+ expect(existsSync(join(featureDir, "plan.md"))).toBe(true);
187
+ expect(existsSync(join(featureDir, "tasks.md"))).toBe(true);
188
+ });
189
+
190
+ test("spec.md contains WHAT/WHY sections only (no HOW)", async () => {
191
+ await scaffoldFeature(featureDir);
192
+ const content = readFileSync(join(featureDir, "spec.md"), "utf-8");
193
+ // WHAT/WHY sections
194
+ expect(content).toContain("Feature:");
195
+ expect(content).toContain("User Stories");
196
+ expect(content).toContain("Success Criteria");
197
+ expect(content).toContain("Problem Statement");
198
+ expect(content).toContain("Out of Scope");
199
+ // Must NOT contain HOW sections
200
+ expect(content).not.toContain("## Architecture");
201
+ expect(content).not.toContain("## Tasks");
202
+ });
203
+
204
+ test("plan.md contains HOW sections only", async () => {
205
+ await scaffoldFeature(featureDir);
206
+ const content = readFileSync(join(featureDir, "plan.md"), "utf-8");
207
+ // HOW sections
208
+ expect(content).toContain("Architecture");
209
+ expect(content).toContain("Tasks");
210
+ expect(content).toContain("Failure Modes");
211
+ expect(content).toContain("Testing Strategy");
212
+ // Must NOT contain WHAT/WHY sections
213
+ expect(content).not.toContain("User Stories");
214
+ expect(content).not.toContain("Success Criteria");
215
+ });
216
+
217
+ test("all files contain [NEEDS CLARIFICATION] marker", async () => {
218
+ await scaffoldFeature(featureDir);
219
+ const spec = readFileSync(join(featureDir, "spec.md"), "utf-8");
220
+ const plan = readFileSync(join(featureDir, "plan.md"), "utf-8");
221
+ const tasks = readFileSync(join(featureDir, "tasks.md"), "utf-8");
222
+ expect(spec).toContain("[NEEDS CLARIFICATION]");
223
+ expect(plan).toContain("[NEEDS CLARIFICATION]");
224
+ expect(tasks).toContain("[NEEDS CLARIFICATION]");
225
+ });
226
+
227
+ test("returns error if featureDir does not exist", async () => {
228
+ const badDir = join(tmpDir, "nonexistent");
229
+ const result = await scaffoldFeature(badDir);
230
+ expect(result.ok).toBe(false);
231
+ if (!result.ok) {
232
+ expect(result.error).toContain("does not exist");
233
+ }
234
+ });
235
+ });
236
+
237
+ describe("scaffoldFeatureWithContext", () => {
238
+ let tmpDir: string;
239
+
240
+ beforeEach(() => {
241
+ tmpDir = makeTmpDir();
242
+ });
243
+
244
+ afterEach(() => {
245
+ rmSync(tmpDir, { recursive: true, force: true });
246
+ });
247
+
248
+ test("creates enriched spec.md with description and tradeoffs", async () => {
249
+ mkdirSync(tmpDir, { recursive: true });
250
+ const choices: DesignChoices = {
251
+ description: "User authentication system with OAuth support",
252
+ pattern: "service-layer",
253
+ libraries: ["passport.js", "jsonwebtoken"],
254
+ tradeoffs: ["Chose simplicity over enterprise features for MVP"],
255
+ clarifications: [
256
+ { question: "Support OAuth?", answer: "Yes, Google and GitHub" },
257
+ ],
258
+ };
259
+
260
+ const result = await scaffoldFeatureWithContext(
261
+ tmpDir,
262
+ "user-auth",
263
+ choices,
264
+ );
265
+ expect(result.ok).toBe(true);
266
+
267
+ const spec = readFileSync(join(tmpDir, "spec.md"), "utf-8");
268
+ expect(spec).toContain("User authentication system with OAuth support");
269
+ expect(spec).toContain("Chose simplicity over enterprise features for MVP");
270
+ expect(spec).toContain("Support OAuth?");
271
+ expect(spec).toContain("Yes, Google and GitHub");
272
+ // Spec should NOT contain implementation details (HOW)
273
+ expect(spec).not.toContain("service-layer");
274
+ expect(spec).not.toContain("passport.js");
275
+ });
276
+
277
+ test("creates enriched plan.md with pattern and libraries", async () => {
278
+ mkdirSync(tmpDir, { recursive: true });
279
+ const choices: DesignChoices = {
280
+ pattern: "repository",
281
+ libraries: ["drizzle", "zod"],
282
+ };
283
+
284
+ const result = await scaffoldFeatureWithContext(
285
+ tmpDir,
286
+ "data-layer",
287
+ choices,
288
+ );
289
+ expect(result.ok).toBe(true);
290
+
291
+ const plan = readFileSync(join(tmpDir, "plan.md"), "utf-8");
292
+ expect(plan).toContain("repository");
293
+ expect(plan).toContain("drizzle");
294
+ expect(plan).toContain("zod");
295
+ // Plan should NOT contain WHAT/WHY
296
+ expect(plan).toContain("HOW only");
297
+ });
298
+
299
+ test("falls back to generic markers when choices are empty", async () => {
300
+ mkdirSync(tmpDir, { recursive: true });
301
+ const choices: DesignChoices = {};
302
+
303
+ const result = await scaffoldFeatureWithContext(tmpDir, "simple", choices);
304
+ expect(result.ok).toBe(true);
305
+
306
+ const plan = readFileSync(join(tmpDir, "plan.md"), "utf-8");
307
+ expect(plan).toContain("[NEEDS CLARIFICATION]");
308
+ });
309
+
310
+ test("creates tasks.md with standard template", async () => {
311
+ mkdirSync(tmpDir, { recursive: true });
312
+ const choices: DesignChoices = { description: "Test feature" };
313
+
314
+ await scaffoldFeatureWithContext(tmpDir, "test", choices);
315
+
316
+ const tasks = readFileSync(join(tmpDir, "tasks.md"), "utf-8");
317
+ expect(tasks).toContain("Task Breakdown");
318
+ });
319
+ });
@@ -0,0 +1,295 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { scoreSpec } from "../quality";
6
+
7
+ function makeTmpDir(): string {
8
+ const dir = join(
9
+ tmpdir(),
10
+ `maina-quality-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
11
+ );
12
+ mkdirSync(dir, { recursive: true });
13
+ return dir;
14
+ }
15
+
16
+ describe("scoreSpec", () => {
17
+ let tmpDir: string;
18
+
19
+ beforeEach(() => {
20
+ tmpDir = makeTmpDir();
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ // --- Good spec with measurable verbs → score > 70 ---
28
+ test("good spec with measurable verbs scores > 70", () => {
29
+ const specPath = join(tmpDir, "spec.md");
30
+ writeFileSync(
31
+ specPath,
32
+ `# Feature: Token Budget
33
+
34
+ ## Problem Statement
35
+ The context engine needs a configurable token budget.
36
+
37
+ ## User Stories
38
+ - As a developer, I want to configure token budgets per command.
39
+
40
+ ## Success Criteria
41
+ - \`calculateTokens\` returns the correct token count for a given input
42
+ - \`assembleContext\` validates that tokens stay within budget of 4000
43
+ - \`parseConfig\` reads the \`.maina/config.json\` file and returns parsed settings
44
+ - The budget engine computes utilization as \`tokens / budget\`
45
+ - \`rejectOverBudget\` throws when context exceeds the configured limit
46
+
47
+ ## Scope
48
+ - In scope: token counting, budget configuration
49
+ - Out of scope: streaming, multi-model
50
+
51
+ ## Design Decisions
52
+ - Use tree-sitter for AST parsing
53
+ `,
54
+ );
55
+
56
+ const result = scoreSpec(specPath);
57
+ expect(result.ok).toBe(true);
58
+ if (!result.ok) return;
59
+ expect(result.value.overall).toBeGreaterThan(70);
60
+ });
61
+
62
+ // --- Spec with vague verbs → measurability < 50 ---
63
+ test("spec with vague verbs has measurability < 50", () => {
64
+ const specPath = join(tmpDir, "spec.md");
65
+ writeFileSync(
66
+ specPath,
67
+ `# Feature: Auth
68
+
69
+ ## Problem Statement
70
+ We need auth.
71
+
72
+ ## User Stories
73
+ - As a user, I want to log in.
74
+
75
+ ## Success Criteria
76
+ - The system handles user authentication
77
+ - The module manages session state
78
+ - The service supports OAuth providers
79
+ - The component processes login requests
80
+ - The layer deals with token refresh
81
+
82
+ ## Scope
83
+ - Auth
84
+
85
+ ## Design Decisions
86
+ - TBD
87
+ `,
88
+ );
89
+
90
+ const result = scoreSpec(specPath);
91
+ expect(result.ok).toBe(true);
92
+ if (!result.ok) return;
93
+ expect(result.value.measurability).toBeLessThan(50);
94
+ });
95
+
96
+ // --- Spec with weasel words → ambiguity < 50 ---
97
+ test("spec with weasel words has ambiguity < 50", () => {
98
+ const specPath = join(tmpDir, "spec.md");
99
+ writeFileSync(
100
+ specPath,
101
+ `# Feature: Maybe Auth
102
+
103
+ ## Problem Statement
104
+ This might possibly solve some authentication issues.
105
+
106
+ ## User Stories
107
+ - As a user, I should possibly be able to log in maybe.
108
+
109
+ ## Success Criteria
110
+ - The system should maybe validate credentials
111
+ - Various appropriate methods could be used
112
+ - Some users might possibly need sessions
113
+ - It should probably handle various edge cases
114
+ - The module could possibly support some providers
115
+ - Maybe it should also handle appropriate errors
116
+
117
+ ## Scope
118
+ - Various things
119
+
120
+ ## Design Decisions
121
+ - Possibly use some appropriate library
122
+ `,
123
+ );
124
+
125
+ const result = scoreSpec(specPath);
126
+ expect(result.ok).toBe(true);
127
+ if (!result.ok) return;
128
+ expect(result.value.ambiguity).toBeLessThan(50);
129
+ });
130
+
131
+ // --- Spec with all sections → completeness = 100 ---
132
+ test("spec with all required sections has completeness 100", () => {
133
+ const specPath = join(tmpDir, "spec.md");
134
+ writeFileSync(
135
+ specPath,
136
+ `# Feature: Complete
137
+
138
+ ## Problem Statement
139
+ Something.
140
+
141
+ ## User Stories
142
+ - Story.
143
+
144
+ ## Success Criteria
145
+ - Criterion.
146
+
147
+ ## Scope
148
+ - Scope.
149
+
150
+ ## Design Decisions
151
+ - Decision.
152
+ `,
153
+ );
154
+
155
+ const result = scoreSpec(specPath);
156
+ expect(result.ok).toBe(true);
157
+ if (!result.ok) return;
158
+ expect(result.value.completeness).toBe(100);
159
+ });
160
+
161
+ // --- Spec missing sections → completeness < 100 ---
162
+ test("spec missing sections has completeness < 100", () => {
163
+ const specPath = join(tmpDir, "spec.md");
164
+ writeFileSync(
165
+ specPath,
166
+ `# Feature: Incomplete
167
+
168
+ ## Problem Statement
169
+ Something.
170
+
171
+ ## Success Criteria
172
+ - Criterion.
173
+ `,
174
+ );
175
+
176
+ const result = scoreSpec(specPath);
177
+ expect(result.ok).toBe(true);
178
+ if (!result.ok) return;
179
+ expect(result.value.completeness).toBeLessThan(100);
180
+ });
181
+
182
+ // --- Spec with [NEEDS CLARIFICATION] → completeness penalized ---
183
+ test("spec with [NEEDS CLARIFICATION] has completeness penalized", () => {
184
+ const specPath = join(tmpDir, "spec.md");
185
+ writeFileSync(
186
+ specPath,
187
+ `# Feature: Unclear
188
+
189
+ ## Problem Statement
190
+ Something. [NEEDS CLARIFICATION]
191
+
192
+ ## User Stories
193
+ - Story. [NEEDS CLARIFICATION]
194
+
195
+ ## Success Criteria
196
+ - Criterion validates input.
197
+
198
+ ## Scope
199
+ - Scope.
200
+
201
+ ## Design Decisions
202
+ - Decision.
203
+ `,
204
+ );
205
+
206
+ const result = scoreSpec(specPath);
207
+ expect(result.ok).toBe(true);
208
+ if (!result.ok) return;
209
+ // All sections present = 100, minus 2 * 10 = 80
210
+ expect(result.value.completeness).toBe(80);
211
+ });
212
+
213
+ // --- Spec with backtick identifiers → testability high ---
214
+ test("spec with backtick identifiers has high testability", () => {
215
+ const specPath = join(tmpDir, "spec.md");
216
+ writeFileSync(
217
+ specPath,
218
+ `# Feature: Testable
219
+
220
+ ## Problem Statement
221
+ Need testable criteria.
222
+
223
+ ## User Stories
224
+ - As a developer, I want clear criteria.
225
+
226
+ ## Success Criteria
227
+ - \`parseInput\` returns a valid AST node
228
+ - The output file is written to \`/tmp/output.json\`
229
+ - The function returns error code 404 when not found
230
+ - \`validateEmail\` rejects strings without @ symbol
231
+ - The response contains exactly 5 items
232
+
233
+ ## Scope
234
+ - Parsing and validation
235
+
236
+ ## Design Decisions
237
+ - Use tree-sitter
238
+ `,
239
+ );
240
+
241
+ const result = scoreSpec(specPath);
242
+ expect(result.ok).toBe(true);
243
+ if (!result.ok) return;
244
+ expect(result.value.testability).toBeGreaterThan(70);
245
+ });
246
+
247
+ // --- Empty spec → score 0 ---
248
+ test("empty spec scores 0", () => {
249
+ const specPath = join(tmpDir, "spec.md");
250
+ writeFileSync(specPath, "");
251
+
252
+ const result = scoreSpec(specPath);
253
+ expect(result.ok).toBe(true);
254
+ if (!result.ok) return;
255
+ expect(result.value.overall).toBe(0);
256
+ });
257
+
258
+ // --- File not found → error Result ---
259
+ test("file not found returns error Result", () => {
260
+ const result = scoreSpec(join(tmpDir, "nonexistent.md"));
261
+ expect(result.ok).toBe(false);
262
+ if (result.ok) return;
263
+ expect(result.error).toContain("not found");
264
+ });
265
+
266
+ // --- Details array is populated ---
267
+ test("details array is populated with human-readable breakdown", () => {
268
+ const specPath = join(tmpDir, "spec.md");
269
+ writeFileSync(
270
+ specPath,
271
+ `# Feature: Details
272
+
273
+ ## Problem Statement
274
+ Something.
275
+
276
+ ## User Stories
277
+ - Story.
278
+
279
+ ## Success Criteria
280
+ - \`validate\` returns true for valid input
281
+
282
+ ## Scope
283
+ - Scope.
284
+
285
+ ## Design Decisions
286
+ - Decision.
287
+ `,
288
+ );
289
+
290
+ const result = scoreSpec(specPath);
291
+ expect(result.ok).toBe(true);
292
+ if (!result.ok) return;
293
+ expect(result.value.details.length).toBeGreaterThan(0);
294
+ });
295
+ });