@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.
- package/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- 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
|
+
});
|