@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,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
+ }