@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,173 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getContextDb } from "../../db/index.ts";
6
+ import { generateDependencyDiagram, generateModuleSummary } from "../index.ts";
7
+
8
+ const TEST_DIR = join(tmpdir(), `maina-explain-test-${Date.now()}`);
9
+
10
+ beforeAll(() => {
11
+ mkdirSync(TEST_DIR, { recursive: true });
12
+ });
13
+
14
+ afterAll(() => {
15
+ rmSync(TEST_DIR, { recursive: true, force: true });
16
+ });
17
+
18
+ function makeDir(sub: string): string {
19
+ const d = join(TEST_DIR, sub);
20
+ mkdirSync(d, { recursive: true });
21
+ return d;
22
+ }
23
+
24
+ // ── generateDependencyDiagram ───────────────────────────────────────────────
25
+
26
+ describe("generateDependencyDiagram", () => {
27
+ test("no edges returns empty graph", () => {
28
+ const mainaDir = makeDir("diagram-empty");
29
+ const result = generateDependencyDiagram(mainaDir);
30
+ expect(result.ok).toBe(true);
31
+ if (!result.ok) return;
32
+ expect(result.value).toBe("graph LR\n");
33
+ });
34
+
35
+ test("with edges returns valid Mermaid syntax", () => {
36
+ const mainaDir = makeDir("diagram-edges");
37
+
38
+ const dbResult = getContextDb(mainaDir);
39
+ expect(dbResult.ok).toBe(true);
40
+ if (!dbResult.ok) return;
41
+ const { db } = dbResult.value;
42
+
43
+ db.exec(`
44
+ INSERT INTO dependency_edges (id, source_file, target_file, weight, type)
45
+ VALUES
46
+ ('e1', 'src/context/engine.ts', 'src/context/budget.ts', 1.0, 'import'),
47
+ ('e2', 'src/context/engine.ts', 'src/context/selector.ts', 1.0, 'import'),
48
+ ('e3', 'src/verify/pipeline.ts', 'src/verify/syntax-guard.ts', 1.0, 'import')
49
+ `);
50
+
51
+ const result = generateDependencyDiagram(mainaDir);
52
+ expect(result.ok).toBe(true);
53
+ if (!result.ok) return;
54
+
55
+ expect(result.value).toContain("graph LR");
56
+ expect(result.value).toContain("context/engine --> context/budget");
57
+ expect(result.value).toContain("context/engine --> context/selector");
58
+ expect(result.value).toContain("verify/pipeline --> verify/syntax-guard");
59
+ db.close();
60
+ });
61
+
62
+ test("with scope filter only returns matching edges", () => {
63
+ const mainaDir = makeDir("diagram-scope");
64
+
65
+ const dbResult = getContextDb(mainaDir);
66
+ expect(dbResult.ok).toBe(true);
67
+ if (!dbResult.ok) return;
68
+ const { db } = dbResult.value;
69
+
70
+ db.exec(`
71
+ INSERT INTO dependency_edges (id, source_file, target_file, weight, type)
72
+ VALUES
73
+ ('e1', 'src/context/engine.ts', 'src/context/budget.ts', 1.0, 'import'),
74
+ ('e2', 'src/verify/pipeline.ts', 'src/verify/syntax-guard.ts', 1.0, 'import'),
75
+ ('e3', 'src/context/engine.ts', 'src/verify/detect.ts', 0.5, 'reference')
76
+ `);
77
+
78
+ const result = generateDependencyDiagram(mainaDir, {
79
+ scope: "context",
80
+ });
81
+ expect(result.ok).toBe(true);
82
+ if (!result.ok) return;
83
+
84
+ expect(result.value).toContain("context/engine --> context/budget");
85
+ // Edge e3 involves context, so it should be included
86
+ expect(result.value).toContain("context/engine --> verify/detect");
87
+ // Edge e2 is verify-only, should not be included
88
+ expect(result.value).not.toContain(
89
+ "verify/pipeline --> verify/syntax-guard",
90
+ );
91
+ db.close();
92
+ });
93
+
94
+ test("DB error returns error Result", () => {
95
+ // Use a path that can't be a DB (e.g. inside a non-existent deeply nested path with null bytes)
96
+ const result = generateDependencyDiagram("/dev/null/nonexistent");
97
+ expect(result.ok).toBe(false);
98
+ if (result.ok) return;
99
+ expect(typeof result.error).toBe("string");
100
+ });
101
+ });
102
+
103
+ // ── generateModuleSummary ───────────────────────────────────────────────────
104
+
105
+ describe("generateModuleSummary", () => {
106
+ test("no entities returns empty array", () => {
107
+ const mainaDir = makeDir("summary-empty");
108
+ const result = generateModuleSummary(mainaDir);
109
+ expect(result.ok).toBe(true);
110
+ if (!result.ok) return;
111
+ expect(result.value).toEqual([]);
112
+ });
113
+
114
+ test("with entities returns correct counts per module", () => {
115
+ const mainaDir = makeDir("summary-entities");
116
+
117
+ const dbResult = getContextDb(mainaDir);
118
+ expect(dbResult.ok).toBe(true);
119
+ if (!dbResult.ok) return;
120
+ const { db } = dbResult.value;
121
+
122
+ db.exec(`
123
+ INSERT INTO semantic_entities (id, file_path, name, kind, start_line, end_line, updated_at)
124
+ VALUES
125
+ ('s1', 'src/context/engine.ts', 'assembleContext', 'function', 1, 50, '2026-01-01'),
126
+ ('s2', 'src/context/engine.ts', 'ContextOptions', 'interface', 51, 60, '2026-01-01'),
127
+ ('s3', 'src/context/budget.ts', 'calculateTokens', 'function', 1, 30, '2026-01-01'),
128
+ ('s4', 'src/verify/pipeline.ts', 'runPipeline', 'function', 1, 100, '2026-01-01'),
129
+ ('s5', 'src/verify/pipeline.ts', 'PipelineResult', 'type', 101, 110, '2026-01-01'),
130
+ ('s6', 'src/verify/pipeline.ts', 'PipelineRunner', 'class', 111, 200, '2026-01-01')
131
+ `);
132
+
133
+ const result = generateModuleSummary(mainaDir);
134
+ expect(result.ok).toBe(true);
135
+ if (!result.ok) return;
136
+
137
+ const summaries = result.value;
138
+
139
+ // Should have entries for context/engine, context/budget, verify/pipeline
140
+ expect(summaries.length).toBe(3);
141
+
142
+ const contextEngine = summaries.find((s) => s.module === "context/engine");
143
+ expect(contextEngine).toBeDefined();
144
+ expect(contextEngine?.entityCount).toBe(2);
145
+ expect(contextEngine?.functions).toBe(1);
146
+ expect(contextEngine?.interfaces).toBe(1);
147
+ expect(contextEngine?.classes).toBe(0);
148
+ expect(contextEngine?.types).toBe(0);
149
+
150
+ const verifyPipeline = summaries.find(
151
+ (s) => s.module === "verify/pipeline",
152
+ );
153
+ expect(verifyPipeline).toBeDefined();
154
+ expect(verifyPipeline?.entityCount).toBe(3);
155
+ expect(verifyPipeline?.functions).toBe(1);
156
+ expect(verifyPipeline?.classes).toBe(1);
157
+ expect(verifyPipeline?.types).toBe(1);
158
+
159
+ const contextBudget = summaries.find((s) => s.module === "context/budget");
160
+ expect(contextBudget).toBeDefined();
161
+ expect(contextBudget?.entityCount).toBe(1);
162
+ expect(contextBudget?.functions).toBe(1);
163
+
164
+ db.close();
165
+ });
166
+
167
+ test("DB error returns error Result", () => {
168
+ const result = generateModuleSummary("/dev/null/nonexistent");
169
+ expect(result.ok).toBe(false);
170
+ if (result.ok) return;
171
+ expect(typeof result.error).toBe("string");
172
+ });
173
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Explain Module — Generate Mermaid dependency diagrams and module summaries.
3
+ *
4
+ * Uses the Context Engine semantic layer (dependency_edges and semantic_entities
5
+ * tables) to visualize codebase structure.
6
+ */
7
+
8
+ import type { Result } from "../db/index.ts";
9
+ import { getContextDb } from "../db/index.ts";
10
+
11
+ // ── Types ────────────────────────────────────────────────────────────────────
12
+
13
+ export interface ModuleSummary {
14
+ module: string;
15
+ entityCount: number;
16
+ functions: number;
17
+ classes: number;
18
+ interfaces: number;
19
+ types: number;
20
+ }
21
+
22
+ export interface DiagramOptions {
23
+ scope?: string;
24
+ }
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Extract a short module label from a file path.
30
+ * e.g. "src/context/engine.ts" -> "context/engine"
31
+ * "context/budget.ts" -> "context/budget"
32
+ */
33
+ function toModuleLabel(filePath: string): string {
34
+ // Strip leading src/ if present
35
+ const stripped = filePath.replace(/^src\//, "");
36
+ // Remove file extension
37
+ return stripped.replace(/\.[^/.]+$/, "");
38
+ }
39
+
40
+ // ── generateDependencyDiagram ────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Read the dependency graph from the Context Engine DB (dependency_edges table)
44
+ * and generate a Mermaid flowchart diagram string.
45
+ *
46
+ * If `scope` is provided, filters to only edges where source or target
47
+ * involves that directory.
48
+ *
49
+ * Returns a Mermaid markdown string on success, or an error on failure.
50
+ * Never throws.
51
+ */
52
+ export function generateDependencyDiagram(
53
+ mainaDir: string,
54
+ options?: DiagramOptions,
55
+ ): Result<string> {
56
+ try {
57
+ const dbResult = getContextDb(mainaDir);
58
+ if (!dbResult.ok) return { ok: false, error: dbResult.error };
59
+
60
+ const { db } = dbResult.value;
61
+
62
+ const rows = db
63
+ .prepare("SELECT source_file, target_file FROM dependency_edges")
64
+ .all() as Array<{ source_file: string; target_file: string }>;
65
+
66
+ let lines = rows.map((row) => ({
67
+ source: toModuleLabel(row.source_file),
68
+ target: toModuleLabel(row.target_file),
69
+ }));
70
+
71
+ // Apply scope filter if provided
72
+ if (options?.scope) {
73
+ const scope = options.scope;
74
+ lines = lines.filter(
75
+ (edge) =>
76
+ edge.source.startsWith(scope) || edge.target.startsWith(scope),
77
+ );
78
+ }
79
+
80
+ // Build Mermaid diagram
81
+ let diagram = "graph LR\n";
82
+ for (const edge of lines) {
83
+ diagram += ` ${edge.source} --> ${edge.target}\n`;
84
+ }
85
+
86
+ db.close();
87
+ return { ok: true, value: diagram };
88
+ } catch (e) {
89
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
90
+ }
91
+ }
92
+
93
+ // ── generateModuleSummary ───────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Read semantic_entities table from Context Engine DB, group entities by
97
+ * file/module, and return a summary of each module.
98
+ *
99
+ * Returns an array of ModuleSummary on success, or an error on failure.
100
+ * Never throws.
101
+ */
102
+ export function generateModuleSummary(
103
+ mainaDir: string,
104
+ ): Result<ModuleSummary[]> {
105
+ try {
106
+ const dbResult = getContextDb(mainaDir);
107
+ if (!dbResult.ok) return { ok: false, error: dbResult.error };
108
+
109
+ const { db } = dbResult.value;
110
+
111
+ const rows = db
112
+ .prepare("SELECT file_path, kind FROM semantic_entities")
113
+ .all() as Array<{ file_path: string; kind: string }>;
114
+
115
+ if (rows.length === 0) {
116
+ db.close();
117
+ return { ok: true, value: [] };
118
+ }
119
+
120
+ // Group by module label
121
+ const moduleMap = new Map<
122
+ string,
123
+ {
124
+ functions: number;
125
+ classes: number;
126
+ interfaces: number;
127
+ types: number;
128
+ total: number;
129
+ }
130
+ >();
131
+
132
+ for (const row of rows) {
133
+ const mod = toModuleLabel(row.file_path);
134
+ const entry = moduleMap.get(mod) ?? {
135
+ functions: 0,
136
+ classes: 0,
137
+ interfaces: 0,
138
+ types: 0,
139
+ total: 0,
140
+ };
141
+
142
+ entry.total++;
143
+ switch (row.kind) {
144
+ case "function":
145
+ entry.functions++;
146
+ break;
147
+ case "class":
148
+ entry.classes++;
149
+ break;
150
+ case "interface":
151
+ entry.interfaces++;
152
+ break;
153
+ case "type":
154
+ entry.types++;
155
+ break;
156
+ }
157
+
158
+ moduleMap.set(mod, entry);
159
+ }
160
+
161
+ const summaries: ModuleSummary[] = [];
162
+ for (const [mod, entry] of moduleMap) {
163
+ summaries.push({
164
+ module: mod,
165
+ entityCount: entry.total,
166
+ functions: entry.functions,
167
+ classes: entry.classes,
168
+ interfaces: entry.interfaces,
169
+ types: entry.types,
170
+ });
171
+ }
172
+
173
+ // Sort by module name for deterministic output
174
+ summaries.sort((a, b) => a.module.localeCompare(b.module));
175
+
176
+ db.close();
177
+ return { ok: true, value: summaries };
178
+ } catch (e) {
179
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
180
+ }
181
+ }
@@ -0,0 +1,358 @@
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 { analyze } from "../analyzer";
6
+
7
+ function makeTmpDir(): string {
8
+ const dir = join(
9
+ tmpdir(),
10
+ `maina-analyzer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
11
+ );
12
+ mkdirSync(dir, { recursive: true });
13
+ return dir;
14
+ }
15
+
16
+ const SPEC_CONTENT = `# Feature: User Auth
17
+
18
+ ## User stories
19
+ - As a user, I want to log in with email/password
20
+
21
+ ## Acceptance criteria
22
+ - Login validates email format
23
+ - Failed login shows error message
24
+ `;
25
+
26
+ const PLAN_CONTENT = `# Implementation Plan
27
+
28
+ ## Architecture
29
+ - JWT authentication with bcrypt
30
+
31
+ ## Tasks
32
+ - T001: Implement login with email format validation
33
+ - T002: Add error messages for failed login attempts
34
+ `;
35
+
36
+ const TASKS_CONTENT = `# Tasks
37
+
38
+ - [ ] T001: Implement login with email format validation
39
+ - [ ] T002: Add error messages for failed login attempts
40
+ `;
41
+
42
+ describe("analyze", () => {
43
+ let tmpDir: string;
44
+
45
+ beforeEach(() => {
46
+ tmpDir = makeTmpDir();
47
+ });
48
+
49
+ afterEach(() => {
50
+ rmSync(tmpDir, { recursive: true, force: true });
51
+ });
52
+
53
+ // --- Feature dir doesn't exist → error Result ---
54
+ test("non-existent feature dir returns error Result", () => {
55
+ const result = analyze(join(tmpDir, "non-existent"));
56
+ expect(result.ok).toBe(false);
57
+ if (!result.ok) {
58
+ expect(result.error).toContain("does not exist");
59
+ }
60
+ });
61
+
62
+ // --- All three files present and consistent → no findings ---
63
+ test("all three consistent files produce no findings", () => {
64
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
65
+ writeFileSync(join(tmpDir, "plan.md"), PLAN_CONTENT);
66
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
67
+
68
+ const result = analyze(tmpDir);
69
+ expect(result.ok).toBe(true);
70
+ if (!result.ok) return;
71
+
72
+ expect(result.value.findings).toHaveLength(0);
73
+ expect(result.value.summary.errors).toBe(0);
74
+ expect(result.value.summary.warnings).toBe(0);
75
+ expect(result.value.summary.info).toBe(0);
76
+ });
77
+
78
+ // --- Missing spec.md → finding with category "missing-file" ---
79
+ test("missing spec.md produces missing-file finding", () => {
80
+ writeFileSync(join(tmpDir, "plan.md"), PLAN_CONTENT);
81
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
82
+
83
+ const result = analyze(tmpDir);
84
+ expect(result.ok).toBe(true);
85
+ if (!result.ok) return;
86
+
87
+ const missingFile = result.value.findings.find(
88
+ (f) => f.category === "missing-file" && f.file === "spec.md",
89
+ );
90
+ expect(missingFile).toBeDefined();
91
+ expect(missingFile?.severity).toBe("warning");
92
+ });
93
+
94
+ // --- Missing plan.md → finding with category "missing-file" ---
95
+ test("missing plan.md produces missing-file finding", () => {
96
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
97
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
98
+
99
+ const result = analyze(tmpDir);
100
+ expect(result.ok).toBe(true);
101
+ if (!result.ok) return;
102
+
103
+ const missingFile = result.value.findings.find(
104
+ (f) => f.category === "missing-file" && f.file === "plan.md",
105
+ );
106
+ expect(missingFile).toBeDefined();
107
+ expect(missingFile?.severity).toBe("warning");
108
+ });
109
+
110
+ // --- Missing tasks.md → finding with category "missing-file" ---
111
+ test("missing tasks.md produces missing-file finding", () => {
112
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
113
+ writeFileSync(join(tmpDir, "plan.md"), PLAN_CONTENT);
114
+
115
+ const result = analyze(tmpDir);
116
+ expect(result.ok).toBe(true);
117
+ if (!result.ok) return;
118
+
119
+ const missingFile = result.value.findings.find(
120
+ (f) => f.category === "missing-file" && f.file === "tasks.md",
121
+ );
122
+ expect(missingFile).toBeDefined();
123
+ expect(missingFile?.severity).toBe("info");
124
+ });
125
+
126
+ // --- Spec criterion not covered by any task → "spec-coverage" error ---
127
+ test("uncovered spec criterion produces spec-coverage error", () => {
128
+ writeFileSync(
129
+ join(tmpDir, "spec.md"),
130
+ `# Feature: User Auth
131
+
132
+ ## Acceptance criteria
133
+ - Login validates email format
134
+ - Failed login shows error message
135
+ - Password reset sends email within 30 seconds
136
+ `,
137
+ );
138
+ writeFileSync(
139
+ join(tmpDir, "plan.md"),
140
+ `# Implementation Plan
141
+
142
+ ## Tasks
143
+ - T001: Implement login with email format validation
144
+ - T002: Add error messages for failed login attempts
145
+ `,
146
+ );
147
+ writeFileSync(
148
+ join(tmpDir, "tasks.md"),
149
+ `# Tasks
150
+
151
+ - [ ] T001: Implement login with email format validation
152
+ - [ ] T002: Add error messages for failed login attempts
153
+ `,
154
+ );
155
+
156
+ const result = analyze(tmpDir);
157
+ expect(result.ok).toBe(true);
158
+ if (!result.ok) return;
159
+
160
+ const coverageFindings = result.value.findings.filter(
161
+ (f) => f.category === "spec-coverage",
162
+ );
163
+ expect(coverageFindings.length).toBeGreaterThan(0);
164
+ expect(coverageFindings[0]?.severity).toBe("error");
165
+ expect(
166
+ coverageFindings.some((f) =>
167
+ f.message.toLowerCase().includes("password reset"),
168
+ ),
169
+ ).toBe(true);
170
+ });
171
+
172
+ // --- Task in plan.md not mapping to spec → "orphaned-task" warning ---
173
+ test("orphaned task in plan.md produces orphaned-task warning", () => {
174
+ writeFileSync(
175
+ join(tmpDir, "spec.md"),
176
+ `# Feature: User Auth
177
+
178
+ ## Acceptance criteria
179
+ - Login validates email format
180
+ `,
181
+ );
182
+ writeFileSync(
183
+ join(tmpDir, "plan.md"),
184
+ `# Implementation Plan
185
+
186
+ ## Tasks
187
+ - T001: Implement login with email format validation
188
+ - T002: Set up Kubernetes deployment pipeline
189
+ `,
190
+ );
191
+ writeFileSync(
192
+ join(tmpDir, "tasks.md"),
193
+ `# Tasks
194
+
195
+ - [ ] T001: Implement login with email format validation
196
+ - [ ] T002: Set up Kubernetes deployment pipeline
197
+ `,
198
+ );
199
+
200
+ const result = analyze(tmpDir);
201
+ expect(result.ok).toBe(true);
202
+ if (!result.ok) return;
203
+
204
+ const orphaned = result.value.findings.filter(
205
+ (f) => f.category === "orphaned-task",
206
+ );
207
+ expect(orphaned.length).toBeGreaterThan(0);
208
+ expect(orphaned[0]?.severity).toBe("warning");
209
+ expect(
210
+ orphaned.some((f) => f.message.toLowerCase().includes("kubernetes")),
211
+ ).toBe(true);
212
+ });
213
+
214
+ // --- spec.md contains implementation keywords → "separation-violation" warning ---
215
+ test("spec.md with implementation keywords produces separation-violation", () => {
216
+ writeFileSync(
217
+ join(tmpDir, "spec.md"),
218
+ `# Feature: User Auth
219
+
220
+ ## Acceptance criteria
221
+ - Login validates email format using JWT endpoint
222
+ - Failed login shows error message via REST API
223
+ `,
224
+ );
225
+ writeFileSync(join(tmpDir, "plan.md"), PLAN_CONTENT);
226
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
227
+
228
+ const result = analyze(tmpDir);
229
+ expect(result.ok).toBe(true);
230
+ if (!result.ok) return;
231
+
232
+ const violations = result.value.findings.filter(
233
+ (f) => f.category === "separation-violation" && f.file === "spec.md",
234
+ );
235
+ expect(violations.length).toBeGreaterThan(0);
236
+ expect(violations[0]?.severity).toBe("warning");
237
+ });
238
+
239
+ // --- plan.md contains user story language → "separation-violation" warning ---
240
+ test("plan.md with user story language produces separation-violation", () => {
241
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
242
+ writeFileSync(
243
+ join(tmpDir, "plan.md"),
244
+ `# Implementation Plan
245
+
246
+ ## Architecture
247
+ - JWT authentication with bcrypt
248
+ - As a user, I want fast login
249
+
250
+ ## Tasks
251
+ - T001: Implement login with email format validation
252
+ - T002: Add error messages for failed login attempts
253
+ `,
254
+ );
255
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
256
+
257
+ const result = analyze(tmpDir);
258
+ expect(result.ok).toBe(true);
259
+ if (!result.ok) return;
260
+
261
+ const violations = result.value.findings.filter(
262
+ (f) => f.category === "separation-violation" && f.file === "plan.md",
263
+ );
264
+ expect(violations.length).toBeGreaterThan(0);
265
+ expect(violations[0]?.severity).toBe("warning");
266
+ });
267
+
268
+ // --- Different task counts between plan.md and tasks.md → "task-consistency" warning ---
269
+ test("different task counts between plan.md and tasks.md produces task-consistency warning", () => {
270
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
271
+ writeFileSync(
272
+ join(tmpDir, "plan.md"),
273
+ `# Implementation Plan
274
+
275
+ ## Tasks
276
+ - T001: Implement login with email format validation
277
+ - T002: Add error messages for failed login attempts
278
+ - T003: Add rate limiting
279
+ `,
280
+ );
281
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
282
+
283
+ const result = analyze(tmpDir);
284
+ expect(result.ok).toBe(true);
285
+ if (!result.ok) return;
286
+
287
+ const consistency = result.value.findings.filter(
288
+ (f) => f.category === "task-consistency",
289
+ );
290
+ expect(consistency.length).toBeGreaterThan(0);
291
+ expect(consistency[0]?.severity).toBe("warning");
292
+ expect(consistency[0]?.message).toContain("3");
293
+ expect(consistency[0]?.message).toContain("2");
294
+ });
295
+
296
+ // --- Contradicting task descriptions → "contradiction" warning ---
297
+ test("contradicting task descriptions produce contradiction warning", () => {
298
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
299
+ writeFileSync(
300
+ join(tmpDir, "plan.md"),
301
+ `# Implementation Plan
302
+
303
+ ## Tasks
304
+ - T001: Implement login with email format validation
305
+ - T002: Add error messages for failed login attempts
306
+ `,
307
+ );
308
+ writeFileSync(
309
+ join(tmpDir, "tasks.md"),
310
+ `# Tasks
311
+
312
+ - [ ] T001: Implement login with email format validation
313
+ - [ ] T002: Build notification service for alerts
314
+ `,
315
+ );
316
+
317
+ const result = analyze(tmpDir);
318
+ expect(result.ok).toBe(true);
319
+ if (!result.ok) return;
320
+
321
+ const contradictions = result.value.findings.filter(
322
+ (f) => f.category === "contradiction",
323
+ );
324
+ expect(contradictions.length).toBeGreaterThan(0);
325
+ expect(contradictions[0]?.severity).toBe("warning");
326
+ expect(contradictions[0]?.message).toContain("T002");
327
+ });
328
+
329
+ // --- Summary correctly counts errors/warnings/info ---
330
+ test("summary correctly counts errors, warnings, and info", () => {
331
+ // Missing spec → warning, missing plan → warning, missing tasks → info
332
+ const result = analyze(tmpDir);
333
+ expect(result.ok).toBe(true);
334
+ if (!result.ok) return;
335
+
336
+ const { summary } = result.value;
337
+ // 3 missing-file findings: spec (warning), plan (warning), tasks (info)
338
+ expect(summary.warnings).toBe(2);
339
+ expect(summary.info).toBe(1);
340
+ expect(summary.errors).toBe(0);
341
+ expect(summary.errors + summary.warnings + summary.info).toBe(
342
+ result.value.findings.length,
343
+ );
344
+ });
345
+
346
+ // --- featureDir is captured in report ---
347
+ test("report includes featureDir", () => {
348
+ writeFileSync(join(tmpDir, "spec.md"), SPEC_CONTENT);
349
+ writeFileSync(join(tmpDir, "plan.md"), PLAN_CONTENT);
350
+ writeFileSync(join(tmpDir, "tasks.md"), TASKS_CONTENT);
351
+
352
+ const result = analyze(tmpDir);
353
+ expect(result.ok).toBe(true);
354
+ if (!result.ok) return;
355
+
356
+ expect(result.value.featureDir).toBe(tmpDir);
357
+ });
358
+ });