@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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec quality scorer.
|
|
3
|
+
*
|
|
4
|
+
* Scores a spec.md file 0-100 based on four dimensions:
|
|
5
|
+
* - Measurability: do criteria use measurable verbs?
|
|
6
|
+
* - Testability: can each criterion be expressed as a test?
|
|
7
|
+
* - Ambiguity: inverse of weasel word count (100 = no ambiguity)
|
|
8
|
+
* - Completeness: are all required sections filled in?
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import type { Result } from "../db/index";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract criteria from both "Acceptance Criteria" and "Success Criteria" sections.
|
|
16
|
+
*/
|
|
17
|
+
function extractCriteria(content: string): string[] {
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
const criteria: string[] = [];
|
|
20
|
+
let inSection = false;
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
|
|
25
|
+
if (/^##\s+(acceptance\s+criteria|success\s+criteria)/i.test(trimmed)) {
|
|
26
|
+
inSection = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (inSection && /^##\s/.test(trimmed)) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (inSection && trimmed.startsWith("-")) {
|
|
35
|
+
const text = trimmed.replace(/^-\s*(\[.\]\s*)?/, "").trim();
|
|
36
|
+
if (text.length > 0) {
|
|
37
|
+
criteria.push(text);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return criteria;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface QualityScore {
|
|
46
|
+
overall: number;
|
|
47
|
+
measurability: number;
|
|
48
|
+
testability: number;
|
|
49
|
+
ambiguity: number;
|
|
50
|
+
completeness: number;
|
|
51
|
+
details: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Measurable verbs that indicate concrete, verifiable behaviour.
|
|
56
|
+
*/
|
|
57
|
+
const MEASURABLE_VERBS = new Set([
|
|
58
|
+
"validates",
|
|
59
|
+
"returns",
|
|
60
|
+
"creates",
|
|
61
|
+
"sends",
|
|
62
|
+
"rejects",
|
|
63
|
+
"throws",
|
|
64
|
+
"writes",
|
|
65
|
+
"reads",
|
|
66
|
+
"parses",
|
|
67
|
+
"computes",
|
|
68
|
+
"generates",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Vague verbs that indicate unclear intent.
|
|
73
|
+
*/
|
|
74
|
+
const VAGUE_VERBS = new Set([
|
|
75
|
+
"handles",
|
|
76
|
+
"manages",
|
|
77
|
+
"supports",
|
|
78
|
+
"processes",
|
|
79
|
+
"deals",
|
|
80
|
+
"takes",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Weasel words that indicate ambiguity.
|
|
85
|
+
*/
|
|
86
|
+
const WEASEL_WORDS = new Set([
|
|
87
|
+
"maybe",
|
|
88
|
+
"might",
|
|
89
|
+
"possibly",
|
|
90
|
+
"should",
|
|
91
|
+
"could",
|
|
92
|
+
"some",
|
|
93
|
+
"various",
|
|
94
|
+
"appropriate",
|
|
95
|
+
"probably",
|
|
96
|
+
"perhaps",
|
|
97
|
+
"fairly",
|
|
98
|
+
"quite",
|
|
99
|
+
"somewhat",
|
|
100
|
+
"arguably",
|
|
101
|
+
"roughly",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Required sections in a well-formed spec.
|
|
106
|
+
*/
|
|
107
|
+
const REQUIRED_SECTIONS = [
|
|
108
|
+
"Problem Statement",
|
|
109
|
+
"User Stories",
|
|
110
|
+
"Success Criteria",
|
|
111
|
+
"Scope",
|
|
112
|
+
"Design Decisions",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Patterns indicating testable criteria: backtick identifiers, specific
|
|
117
|
+
* numbers, file paths, function names, error messages.
|
|
118
|
+
*/
|
|
119
|
+
const TESTABLE_PATTERNS = [
|
|
120
|
+
/`[^`]+`/, // backtick-quoted identifiers
|
|
121
|
+
/\b\d+\b/, // specific numbers
|
|
122
|
+
/\/[\w./]+/, // file paths
|
|
123
|
+
/\b[a-z][a-zA-Z]*[A-Z]\w*\b/, // camelCase function names
|
|
124
|
+
/\b(error|Error|ERROR)\s+(code|message|status)\b/i, // error references
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Score measurability of acceptance criteria.
|
|
129
|
+
* Returns 0 when there are no criteria (empty spec).
|
|
130
|
+
*/
|
|
131
|
+
function scoreMeasurability(criteria: string[]): {
|
|
132
|
+
score: number;
|
|
133
|
+
details: string;
|
|
134
|
+
} {
|
|
135
|
+
if (criteria.length === 0) {
|
|
136
|
+
return { score: 0, details: "Measurability: 0 — no acceptance criteria" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let measurable = 0;
|
|
140
|
+
let vague = 0;
|
|
141
|
+
|
|
142
|
+
for (const criterion of criteria) {
|
|
143
|
+
const lower = criterion.toLowerCase();
|
|
144
|
+
const words = lower.split(/\s+/);
|
|
145
|
+
|
|
146
|
+
const hasMeasurable = words.some((w) => MEASURABLE_VERBS.has(w));
|
|
147
|
+
const hasVague = words.some((w) => VAGUE_VERBS.has(w));
|
|
148
|
+
|
|
149
|
+
// Also check for two-word vague phrases
|
|
150
|
+
const hasVaguePhrase =
|
|
151
|
+
lower.includes("deals with") || lower.includes("takes care of");
|
|
152
|
+
|
|
153
|
+
if (hasMeasurable && !hasVague && !hasVaguePhrase) {
|
|
154
|
+
measurable++;
|
|
155
|
+
} else if (hasVague || hasVaguePhrase) {
|
|
156
|
+
vague++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const total = criteria.length;
|
|
161
|
+
const score = Math.round((measurable / total) * 100);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
score,
|
|
165
|
+
details: `Measurability: ${score} — ${measurable}/${total} criteria use measurable verbs (${vague} vague)`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Score testability of acceptance criteria.
|
|
171
|
+
* Criteria with backtick identifiers, specific numbers, file paths, etc.
|
|
172
|
+
*/
|
|
173
|
+
function scoreTestability(criteria: string[]): {
|
|
174
|
+
score: number;
|
|
175
|
+
details: string;
|
|
176
|
+
} {
|
|
177
|
+
if (criteria.length === 0) {
|
|
178
|
+
return { score: 0, details: "Testability: 0 — no acceptance criteria" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let testable = 0;
|
|
182
|
+
|
|
183
|
+
for (const criterion of criteria) {
|
|
184
|
+
const hasTestablePattern = TESTABLE_PATTERNS.some((p) => p.test(criterion));
|
|
185
|
+
if (hasTestablePattern) {
|
|
186
|
+
testable++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const total = criteria.length;
|
|
191
|
+
const score = Math.round((testable / total) * 100);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
score,
|
|
195
|
+
details: `Testability: ${score} — ${testable}/${total} criteria contain testable patterns`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Score ambiguity across entire spec content.
|
|
201
|
+
* 100 = no weasel words, each weasel word deducts 10 points.
|
|
202
|
+
*/
|
|
203
|
+
function scoreAmbiguity(content: string): { score: number; details: string } {
|
|
204
|
+
if (content.trim().length === 0) {
|
|
205
|
+
return { score: 0, details: "Ambiguity: 0 — empty spec" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const words = content.toLowerCase().split(/\s+/);
|
|
209
|
+
let weaselCount = 0;
|
|
210
|
+
|
|
211
|
+
for (const word of words) {
|
|
212
|
+
// Strip punctuation for matching
|
|
213
|
+
const clean = word.replace(/[^a-z]/g, "");
|
|
214
|
+
if (WEASEL_WORDS.has(clean)) {
|
|
215
|
+
weaselCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const score = Math.max(0, 100 - weaselCount * 10);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
score,
|
|
223
|
+
details: `Ambiguity: ${score} — ${weaselCount} weasel word(s) found`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Score completeness based on required sections and [NEEDS CLARIFICATION] markers.
|
|
229
|
+
*/
|
|
230
|
+
function scoreCompleteness(content: string): {
|
|
231
|
+
score: number;
|
|
232
|
+
details: string;
|
|
233
|
+
} {
|
|
234
|
+
if (content.trim().length === 0) {
|
|
235
|
+
return { score: 0, details: "Completeness: 0 — empty spec" };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let present = 0;
|
|
239
|
+
const missing: string[] = [];
|
|
240
|
+
|
|
241
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
242
|
+
// Check for heading containing the section name (case-insensitive)
|
|
243
|
+
const pattern = new RegExp(`^##\\s+${escapeRegex(section)}`, "im");
|
|
244
|
+
if (pattern.test(content)) {
|
|
245
|
+
present++;
|
|
246
|
+
} else {
|
|
247
|
+
missing.push(section);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let score = Math.round((present / REQUIRED_SECTIONS.length) * 100);
|
|
252
|
+
|
|
253
|
+
// Penalize [NEEDS CLARIFICATION] markers
|
|
254
|
+
const clarificationMatches = content.match(/\[NEEDS CLARIFICATION\]/g);
|
|
255
|
+
const markerCount = clarificationMatches?.length ?? 0;
|
|
256
|
+
if (markerCount > 0) {
|
|
257
|
+
score = Math.max(0, score - markerCount * 10);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const missingStr =
|
|
261
|
+
missing.length > 0 ? ` — missing: ${missing.join(", ")}` : "";
|
|
262
|
+
const markerStr =
|
|
263
|
+
markerCount > 0
|
|
264
|
+
? ` — ${markerCount} [NEEDS CLARIFICATION] marker(s) (-${markerCount * 10})`
|
|
265
|
+
: "";
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
score,
|
|
269
|
+
details: `Completeness: ${score} — ${present}/${REQUIRED_SECTIONS.length} sections present${missingStr}${markerStr}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Escape special regex characters in a string.
|
|
275
|
+
*/
|
|
276
|
+
function escapeRegex(s: string): string {
|
|
277
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Score a spec.md file 0-100 based on four dimensions.
|
|
282
|
+
*
|
|
283
|
+
* - Measurability (25%): measurable verbs in acceptance criteria
|
|
284
|
+
* - Testability (25%): testable patterns in acceptance criteria
|
|
285
|
+
* - Ambiguity (25%): inverse of weasel word count
|
|
286
|
+
* - Completeness (25%): required sections + [NEEDS CLARIFICATION] penalty
|
|
287
|
+
*/
|
|
288
|
+
export function scoreSpec(specPath: string): Result<QualityScore> {
|
|
289
|
+
if (!existsSync(specPath)) {
|
|
290
|
+
return { ok: false, error: `Spec file not found: ${specPath}` };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let content: string;
|
|
294
|
+
try {
|
|
295
|
+
content = readFileSync(specPath, "utf-8");
|
|
296
|
+
} catch (e) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: `Failed to read spec: ${e instanceof Error ? e.message : String(e)}`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Empty spec → all zeros
|
|
304
|
+
if (content.trim().length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
value: {
|
|
308
|
+
overall: 0,
|
|
309
|
+
measurability: 0,
|
|
310
|
+
testability: 0,
|
|
311
|
+
ambiguity: 0,
|
|
312
|
+
completeness: 0,
|
|
313
|
+
details: ["Empty spec file — all dimensions score 0"],
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const criteria = extractCriteria(content);
|
|
319
|
+
|
|
320
|
+
const measurability = scoreMeasurability(criteria);
|
|
321
|
+
const testability = scoreTestability(criteria);
|
|
322
|
+
const ambiguity = scoreAmbiguity(content);
|
|
323
|
+
const completeness = scoreCompleteness(content);
|
|
324
|
+
|
|
325
|
+
const overall = Math.round(
|
|
326
|
+
measurability.score * 0.25 +
|
|
327
|
+
testability.score * 0.25 +
|
|
328
|
+
ambiguity.score * 0.25 +
|
|
329
|
+
completeness.score * 0.25,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
value: {
|
|
335
|
+
overall,
|
|
336
|
+
measurability: measurability.score,
|
|
337
|
+
testability: testability.score,
|
|
338
|
+
ambiguity: ambiguity.score,
|
|
339
|
+
completeness: completeness.score,
|
|
340
|
+
details: [
|
|
341
|
+
measurability.details,
|
|
342
|
+
testability.details,
|
|
343
|
+
ambiguity.details,
|
|
344
|
+
completeness.details,
|
|
345
|
+
`Overall: ${overall} (weighted average)`,
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD test stub generation from plan.md task lists.
|
|
3
|
+
*
|
|
4
|
+
* Parses task lines (- T001: or - [ ] T001:) from plan content and
|
|
5
|
+
* generates bun:test stubs with failing expects (red phase).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Ambiguity Detection ──────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const AMBIGUOUS_PATTERNS = [
|
|
11
|
+
/\bmaybe\b/i,
|
|
12
|
+
/\bmight\b/i,
|
|
13
|
+
/\bpossibly\b/i,
|
|
14
|
+
/\bpossible\b/i,
|
|
15
|
+
/\btbd\b/i,
|
|
16
|
+
/\bor\b/i,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function isAmbiguous(text: string): boolean {
|
|
20
|
+
return AMBIGUOUS_PATTERNS.some((pattern) => pattern.test(text));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Task Parsing ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
interface ParsedTask {
|
|
26
|
+
id: string;
|
|
27
|
+
description: string;
|
|
28
|
+
ambiguous: boolean;
|
|
29
|
+
rawLine: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse task lines from plan.md content.
|
|
34
|
+
* Matches patterns like:
|
|
35
|
+
* - T001: description
|
|
36
|
+
* - [ ] T001: description
|
|
37
|
+
* - [x] T001: description
|
|
38
|
+
*/
|
|
39
|
+
function parseTasks(planContent: string): ParsedTask[] {
|
|
40
|
+
const lines = planContent.split("\n");
|
|
41
|
+
const tasks: ParsedTask[] = [];
|
|
42
|
+
|
|
43
|
+
// Match: - T001: ... or - [ ] T001: ... or - [x] T001: ...
|
|
44
|
+
const taskPattern = /^-\s+(?:\[[ x]\]\s+)?T(\d+):\s*(.+)$/;
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
const match = trimmed.match(taskPattern);
|
|
49
|
+
if (match?.[1] && match[2]) {
|
|
50
|
+
const id = `T${match[1]}`;
|
|
51
|
+
const description = match[2].trim();
|
|
52
|
+
tasks.push({
|
|
53
|
+
id,
|
|
54
|
+
description,
|
|
55
|
+
ambiguous: isAmbiguous(description),
|
|
56
|
+
rawLine: trimmed,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return tasks;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Test Stub Generation ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert a task description to a test-friendly name.
|
|
68
|
+
* Lowercases the first letter and prepends "should".
|
|
69
|
+
*/
|
|
70
|
+
function toTestName(description: string): string {
|
|
71
|
+
const lower = description.charAt(0).toLowerCase() + description.slice(1);
|
|
72
|
+
return `should ${lower}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect if a task handles user input (needs security tests).
|
|
77
|
+
*/
|
|
78
|
+
function handlesInput(description: string): boolean {
|
|
79
|
+
const inputPatterns =
|
|
80
|
+
/\b(input|param|arg|path|file|query|search|body|title|label|name|url|content)\b/i;
|
|
81
|
+
return inputPatterns.test(description);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pure function: parses plan.md content and generates TDD test stubs.
|
|
86
|
+
*
|
|
87
|
+
* - Parses task lines (- T001: or - [ ] T001:)
|
|
88
|
+
* - Creates it() blocks with failing expects (red phase)
|
|
89
|
+
* - Generates five test categories per task: happy path, edge cases, error handling, security, integration
|
|
90
|
+
* - Adds [NEEDS CLARIFICATION] for ambiguous tasks
|
|
91
|
+
* - Returns complete TypeScript test file as a string
|
|
92
|
+
*/
|
|
93
|
+
export function generateTestStubs(
|
|
94
|
+
planContent: string,
|
|
95
|
+
featureName: string,
|
|
96
|
+
): string {
|
|
97
|
+
const tasks = parseTasks(planContent);
|
|
98
|
+
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
|
|
101
|
+
lines.push('import { describe, expect, it } from "bun:test";');
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(`describe("Feature: ${featureName}", () => {`);
|
|
104
|
+
|
|
105
|
+
for (const task of tasks) {
|
|
106
|
+
const testName = toTestName(task.description);
|
|
107
|
+
|
|
108
|
+
if (task.ambiguous) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push(
|
|
111
|
+
`\t// [NEEDS CLARIFICATION] ${task.id}: task description mentions ambiguous language — clarify requirement`,
|
|
112
|
+
);
|
|
113
|
+
lines.push(`\tit("${task.id}: ${testName}", () => {`);
|
|
114
|
+
lines.push(
|
|
115
|
+
"\t\t// [NEEDS CLARIFICATION] Ambiguous requirement — clarify before implementing",
|
|
116
|
+
);
|
|
117
|
+
lines.push("\t\texpect(true).toBe(false); // Red phase");
|
|
118
|
+
lines.push("\t});");
|
|
119
|
+
} else {
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push(`\tdescribe("${task.id}: ${task.description}", () => {`);
|
|
122
|
+
|
|
123
|
+
// Happy path
|
|
124
|
+
lines.push(`\t\tit("happy path: ${testName}", () => {`);
|
|
125
|
+
lines.push("\t\t\texpect(true).toBe(false); // Red phase");
|
|
126
|
+
lines.push("\t\t});");
|
|
127
|
+
|
|
128
|
+
// Edge cases
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push(`\t\tit("edge case: handles empty input", () => {`);
|
|
131
|
+
lines.push("\t\t\texpect(true).toBe(false); // Red phase");
|
|
132
|
+
lines.push("\t\t});");
|
|
133
|
+
|
|
134
|
+
// Error handling
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(`\t\tit("error: returns Result error on failure", () => {`);
|
|
137
|
+
lines.push("\t\t\texpect(true).toBe(false); // Red phase");
|
|
138
|
+
lines.push("\t\t});");
|
|
139
|
+
|
|
140
|
+
// Security (only if task handles input)
|
|
141
|
+
if (handlesInput(task.description)) {
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push(`\t\tit("security: rejects malicious input", () => {`);
|
|
144
|
+
lines.push("\t\t\t// Test path traversal, injection, oversized input");
|
|
145
|
+
lines.push("\t\t\texpect(true).toBe(false); // Red phase");
|
|
146
|
+
lines.push("\t\t});");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
lines.push("\t});");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push("});");
|
|
154
|
+
lines.push("");
|
|
155
|
+
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|