@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,297 @@
1
+ /**
2
+ * Architecture Decision Record (ADR) management.
3
+ *
4
+ * Handles auto-numbering, scaffolding MADR templates, and listing ADRs.
5
+ * ADRs capture WHAT and WHY — no implementation details.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tryAIGenerate } from "../ai/try-generate";
11
+ import type { Result } from "../db/index";
12
+ import { toKebabCase } from "../utils";
13
+
14
+ // ── Types ────────────────────────────────────────────────────────────────────
15
+
16
+ export interface AdrSummary {
17
+ number: string;
18
+ title: string;
19
+ status: string;
20
+ path: string;
21
+ }
22
+
23
+ /**
24
+ * Extract numeric prefix from an ADR filename.
25
+ * Returns the number if the name matches NNNN-*.md pattern, or null.
26
+ */
27
+ function extractNumber(name: string): number | null {
28
+ const match = name.match(/^(\d{4})-.*\.md$/);
29
+ if (!match?.[1]) return null;
30
+ return Number.parseInt(match[1], 10);
31
+ }
32
+
33
+ // ── MADR Template ────────────────────────────────────────────────────────────
34
+
35
+ function buildMadrTemplate(number: string, title: string): string {
36
+ const today = new Date().toISOString().split("T")[0];
37
+ return `# ${number}. ${title}
38
+
39
+ Date: ${today}
40
+
41
+ ## Status
42
+
43
+ Proposed
44
+
45
+ ## Context
46
+
47
+ What is the issue that we're seeing that is motivating this decision or change?
48
+
49
+ [NEEDS CLARIFICATION] Describe the context.
50
+
51
+ ## Decision
52
+
53
+ What is the change that we're proposing and/or doing?
54
+
55
+ [NEEDS CLARIFICATION] Describe the decision.
56
+
57
+ ## Consequences
58
+
59
+ What becomes easier or more difficult to do because of this change?
60
+
61
+ ### Positive
62
+
63
+ - [NEEDS CLARIFICATION]
64
+
65
+ ### Negative
66
+
67
+ - [NEEDS CLARIFICATION]
68
+
69
+ ### Neutral
70
+
71
+ - [NEEDS CLARIFICATION]
72
+
73
+ ## High-Level Design
74
+
75
+ ### System Overview
76
+
77
+ [NEEDS CLARIFICATION]
78
+
79
+ ### Component Boundaries
80
+
81
+ [NEEDS CLARIFICATION]
82
+
83
+ ### Data Flow
84
+
85
+ [NEEDS CLARIFICATION]
86
+
87
+ ### External Dependencies
88
+
89
+ [NEEDS CLARIFICATION]
90
+
91
+ ## Low-Level Design
92
+
93
+ ### Interfaces & Types
94
+
95
+ [NEEDS CLARIFICATION]
96
+
97
+ ### Function Signatures
98
+
99
+ [NEEDS CLARIFICATION]
100
+
101
+ ### DB Schema Changes
102
+
103
+ [NEEDS CLARIFICATION]
104
+
105
+ ### Sequence of Operations
106
+
107
+ [NEEDS CLARIFICATION]
108
+
109
+ ### Error Handling
110
+
111
+ [NEEDS CLARIFICATION]
112
+
113
+ ### Edge Cases
114
+
115
+ [NEEDS CLARIFICATION]
116
+ `;
117
+ }
118
+
119
+ // ── Public API ───────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Scan `adr/` directory for existing ADRs (files named NNNN-*.md),
123
+ * and return the next number zero-padded to 4 digits.
124
+ *
125
+ * Empty dir -> "0001". Existing 0001, 0002 -> "0003".
126
+ * Creates adr/ if it doesn't exist.
127
+ */
128
+ export async function getNextAdrNumber(
129
+ adrDir: string,
130
+ ): Promise<Result<string>> {
131
+ try {
132
+ if (!existsSync(adrDir)) {
133
+ mkdirSync(adrDir, { recursive: true });
134
+ return { ok: true, value: "0001" };
135
+ }
136
+
137
+ const entries = readdirSync(adrDir);
138
+ let maxNumber = 0;
139
+
140
+ for (const entry of entries) {
141
+ const num = extractNumber(entry);
142
+ if (num !== null && num > maxNumber) {
143
+ maxNumber = num;
144
+ }
145
+ }
146
+
147
+ const next = (maxNumber + 1).toString().padStart(4, "0");
148
+ return { ok: true, value: next };
149
+ } catch (e) {
150
+ const message = e instanceof Error ? e.message : String(e);
151
+ return {
152
+ ok: false,
153
+ error: `Failed to get next ADR number: ${message}`,
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create `adr/NNNN-kebab-title.md` using the MADR template.
160
+ * Returns the file path on success.
161
+ */
162
+ export async function scaffoldAdr(
163
+ adrDir: string,
164
+ number: string,
165
+ title: string,
166
+ ): Promise<Result<string>> {
167
+ try {
168
+ const kebabTitle = toKebabCase(title);
169
+ const filename = `${number}-${kebabTitle}.md`;
170
+ const filePath = join(adrDir, filename);
171
+
172
+ if (!existsSync(adrDir)) {
173
+ mkdirSync(adrDir, { recursive: true });
174
+ }
175
+
176
+ const content = buildMadrTemplate(number, title);
177
+ await Bun.write(filePath, content);
178
+
179
+ return { ok: true, value: filePath };
180
+ } catch (e) {
181
+ const message = e instanceof Error ? e.message : String(e);
182
+ return {
183
+ ok: false,
184
+ error: `Failed to scaffold ADR: ${message}`,
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * List existing ADRs with number, title, status.
191
+ * Reads each NNNN-*.md file to extract title (line 1) and status (after ## Status).
192
+ * Returns sorted by number.
193
+ */
194
+ export async function listAdrs(adrDir: string): Promise<Result<AdrSummary[]>> {
195
+ try {
196
+ if (!existsSync(adrDir)) {
197
+ return {
198
+ ok: false,
199
+ error: `ADR directory does not exist: ${adrDir}`,
200
+ };
201
+ }
202
+
203
+ const entries = readdirSync(adrDir);
204
+ const summaries: AdrSummary[] = [];
205
+
206
+ for (const entry of entries) {
207
+ const num = extractNumber(entry);
208
+ if (num === null) continue;
209
+
210
+ const filePath = join(adrDir, entry);
211
+ const content = readFileSync(filePath, "utf-8");
212
+ const lines = content.split("\n");
213
+
214
+ // Extract title from first line: "# NNNN. Title"
215
+ let title = "";
216
+ const titleMatch = lines[0]?.match(/^#\s+\d{4}\.\s+(.+)/);
217
+ if (titleMatch?.[1]) {
218
+ title = titleMatch[1];
219
+ }
220
+
221
+ // Extract status: find "## Status" header, then take next non-empty line
222
+ let status = "Unknown";
223
+ const statusIdx = lines.findIndex((l) =>
224
+ l.trim().toLowerCase().startsWith("## status"),
225
+ );
226
+ if (statusIdx !== -1) {
227
+ for (let i = statusIdx + 1; i < lines.length; i++) {
228
+ const line = lines[i]?.trim();
229
+ if (line && line.length > 0) {
230
+ status = line;
231
+ break;
232
+ }
233
+ }
234
+ }
235
+
236
+ summaries.push({
237
+ number: num.toString().padStart(4, "0"),
238
+ title,
239
+ status,
240
+ path: filePath,
241
+ });
242
+ }
243
+
244
+ summaries.sort((a, b) => a.number.localeCompare(b.number));
245
+
246
+ return { ok: true, value: summaries };
247
+ } catch (e) {
248
+ const message = e instanceof Error ? e.message : String(e);
249
+ return {
250
+ ok: false,
251
+ error: `Failed to list ADRs: ${message}`,
252
+ };
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Generate HLD/LLD sections from a spec using AI (standard tier).
258
+ * Returns the generated markdown content, or null if AI is unavailable.
259
+ */
260
+ export async function generateHldLld(
261
+ specContent: string,
262
+ mainaDir: string,
263
+ ): Promise<Result<string | null>> {
264
+ try {
265
+ const variables: Record<string, string> = {
266
+ spec: specContent,
267
+ conventions: "",
268
+ };
269
+
270
+ const aiResult = await tryAIGenerate(
271
+ "design-hld-lld",
272
+ mainaDir,
273
+ variables,
274
+ `Generate HLD and LLD sections for this spec:\n\n${specContent}`,
275
+ );
276
+
277
+ if (!aiResult.text) {
278
+ if (aiResult.delegation) {
279
+ // Return delegation prompt for host to process
280
+ return {
281
+ ok: true,
282
+ value: `<!-- AI delegation: process this prompt to generate HLD/LLD -->\n\n${aiResult.delegation.userPrompt}`,
283
+ };
284
+ }
285
+ return {
286
+ ok: false,
287
+ error:
288
+ "AI generation unavailable — set MAINA_API_KEY or OPENROUTER_API_KEY to enable HLD/LLD generation",
289
+ };
290
+ }
291
+
292
+ return { ok: true, value: aiResult.text };
293
+ } catch (e) {
294
+ const message = e instanceof Error ? e.message : String(e);
295
+ return { ok: false, error: `HLD/LLD generation failed: ${message}` };
296
+ }
297
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * ADR Design Review.
3
+ *
4
+ * Reviews an Architecture Decision Record against existing ADRs and
5
+ * the project constitution. Performs deterministic checks for MADR
6
+ * section completeness and [NEEDS CLARIFICATION] markers.
7
+ *
8
+ * Single LLM call per command — deterministic checks run without AI.
9
+ */
10
+
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
+ import { basename, join } from "node:path";
13
+ import type { Result } from "../db/index";
14
+ import { loadConstitution } from "../prompts/loader";
15
+
16
+ // ── Types ────────────────────────────────────────────────────────────────────
17
+
18
+ export interface ReviewContext {
19
+ targetAdr: { path: string; content: string; title: string };
20
+ existingAdrs: Array<{ path: string; content: string; title: string }>;
21
+ constitution: string | null;
22
+ }
23
+
24
+ export interface ReviewOptions {
25
+ aiAvailable?: boolean;
26
+ }
27
+
28
+ export interface ReviewFinding {
29
+ severity: "error" | "warning" | "info";
30
+ message: string;
31
+ section?: string;
32
+ }
33
+
34
+ export interface ReviewResult {
35
+ adrPath: string;
36
+ findings: ReviewFinding[];
37
+ passed: boolean; // true if no errors
38
+ sectionsPresent: string[];
39
+ sectionsMissing: string[];
40
+ }
41
+
42
+ // ── Constants ───────────────────────────────────────────────────────────────
43
+
44
+ const REQUIRED_SECTIONS = ["Status", "Context", "Decision", "Consequences"];
45
+
46
+ const HLD_SECTIONS = [
47
+ "System Overview",
48
+ "Component Boundaries",
49
+ "Data Flow",
50
+ "External Dependencies",
51
+ ];
52
+
53
+ const LLD_SECTIONS = [
54
+ "Interfaces & Types",
55
+ "Function Signatures",
56
+ "DB Schema Changes",
57
+ "Sequence of Operations",
58
+ "Error Handling",
59
+ "Edge Cases",
60
+ ];
61
+
62
+ // ── Helpers ─────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Extract the title from an ADR's first line.
66
+ * Expects format: `# NNNN. Title`
67
+ */
68
+ function extractTitle(content: string): string {
69
+ const firstLine = content.split("\n")[0] ?? "";
70
+ const match = firstLine.match(/^#\s+\d{4}\.\s+(.+)/);
71
+ return match?.[1]?.trim() ?? "";
72
+ }
73
+
74
+ /**
75
+ * Extract numeric prefix from an ADR filename.
76
+ * Returns the number if the name matches NNNN-*.md pattern, or null.
77
+ */
78
+ function extractNumber(name: string): number | null {
79
+ const match = name.match(/^(\d{4})-.*\.md$/);
80
+ if (!match?.[1]) return null;
81
+ return Number.parseInt(match[1], 10);
82
+ }
83
+
84
+ /**
85
+ * Check which MADR sections are present in the content.
86
+ */
87
+ function detectSections(content: string): {
88
+ present: string[];
89
+ missing: string[];
90
+ } {
91
+ const present: string[] = [];
92
+ const missing: string[] = [];
93
+
94
+ for (const section of REQUIRED_SECTIONS) {
95
+ // Match ## Section as a heading (case-insensitive)
96
+ const pattern = new RegExp(`^##\\s+${section}\\s*$`, "im");
97
+ if (pattern.test(content)) {
98
+ present.push(section);
99
+ } else {
100
+ missing.push(section);
101
+ }
102
+ }
103
+
104
+ return { present, missing };
105
+ }
106
+
107
+ /**
108
+ * Count occurrences of [NEEDS CLARIFICATION] in content.
109
+ */
110
+ function countClarificationMarkers(content: string): number {
111
+ const matches = content.match(/\[NEEDS CLARIFICATION\]/g);
112
+ return matches?.length ?? 0;
113
+ }
114
+
115
+ // ── Public API ──────────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Build the review context by reading the target ADR, all existing ADRs,
119
+ * and the constitution.
120
+ */
121
+ export async function buildReviewContext(
122
+ adrPath: string,
123
+ adrDir: string,
124
+ mainaDir: string,
125
+ ): Promise<Result<ReviewContext>> {
126
+ try {
127
+ // Read target ADR
128
+ if (!existsSync(adrPath)) {
129
+ return {
130
+ ok: false,
131
+ error: `Target ADR not found: ${adrPath}`,
132
+ };
133
+ }
134
+
135
+ const targetContent = readFileSync(adrPath, "utf-8");
136
+ const targetTitle = extractTitle(targetContent);
137
+
138
+ // Read all other ADRs from adrDir
139
+ const existingAdrs: ReviewContext["existingAdrs"] = [];
140
+
141
+ if (existsSync(adrDir)) {
142
+ const entries = readdirSync(adrDir);
143
+ const targetBasename = basename(adrPath);
144
+
145
+ for (const entry of entries) {
146
+ // Skip target ADR and non-ADR files
147
+ if (entry === targetBasename) continue;
148
+ if (extractNumber(entry) === null) continue;
149
+
150
+ const filePath = join(adrDir, entry);
151
+ const content = readFileSync(filePath, "utf-8");
152
+ const title = extractTitle(content);
153
+
154
+ existingAdrs.push({ path: filePath, content, title });
155
+ }
156
+ }
157
+
158
+ // Read constitution
159
+ const constitutionContent = await loadConstitution(mainaDir);
160
+ const constitution =
161
+ constitutionContent.length > 0 ? constitutionContent : null;
162
+
163
+ return {
164
+ ok: true,
165
+ value: {
166
+ targetAdr: {
167
+ path: adrPath,
168
+ content: targetContent,
169
+ title: targetTitle,
170
+ },
171
+ existingAdrs,
172
+ constitution,
173
+ },
174
+ };
175
+ } catch (e) {
176
+ const message = e instanceof Error ? e.message : String(e);
177
+ return {
178
+ ok: false,
179
+ error: `Failed to build review context: ${message}`,
180
+ };
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Find an ADR file in the adr directory by its number (e.g., "0001").
186
+ */
187
+ export async function findAdrByNumber(
188
+ adrDir: string,
189
+ number: string,
190
+ ): Promise<Result<string>> {
191
+ try {
192
+ if (!existsSync(adrDir)) {
193
+ return {
194
+ ok: false,
195
+ error: `ADR directory does not exist: ${adrDir}`,
196
+ };
197
+ }
198
+
199
+ const paddedNumber = number.padStart(4, "0");
200
+ const entries = readdirSync(adrDir);
201
+
202
+ for (const entry of entries) {
203
+ if (entry.startsWith(`${paddedNumber}-`) && entry.endsWith(".md")) {
204
+ return { ok: true, value: join(adrDir, entry) };
205
+ }
206
+ }
207
+
208
+ return {
209
+ ok: false,
210
+ error: `No ADR found with number ${paddedNumber}`,
211
+ };
212
+ } catch (e) {
213
+ const message = e instanceof Error ? e.message : String(e);
214
+ return {
215
+ ok: false,
216
+ error: `Failed to find ADR: ${message}`,
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Review an ADR using deterministic checks.
223
+ *
224
+ * Checks:
225
+ * 1. Required MADR sections present (Status, Context, Decision, Consequences)
226
+ * 2. [NEEDS CLARIFICATION] markers flagged as incomplete
227
+ */
228
+ export function reviewDesign(
229
+ context: ReviewContext,
230
+ _options?: ReviewOptions,
231
+ ): Result<ReviewResult> {
232
+ try {
233
+ const findings: ReviewFinding[] = [];
234
+ const { content } = context.targetAdr;
235
+
236
+ // Check required sections
237
+ const { present, missing } = detectSections(content);
238
+
239
+ for (const section of missing) {
240
+ findings.push({
241
+ severity: "error",
242
+ message: `Missing required section: "## ${section}"`,
243
+ section,
244
+ });
245
+ }
246
+
247
+ // Check for [NEEDS CLARIFICATION] markers
248
+ // >5 markers means the ADR is effectively empty — error, not warning
249
+ const clarificationCount = countClarificationMarkers(content);
250
+ if (clarificationCount > 5) {
251
+ findings.push({
252
+ severity: "error",
253
+ message: `Contains ${clarificationCount} [NEEDS CLARIFICATION] markers — ADR is effectively empty and should not be committed`,
254
+ });
255
+ } else if (clarificationCount > 0) {
256
+ findings.push({
257
+ severity: "warning",
258
+ message: `Contains ${clarificationCount} [NEEDS CLARIFICATION] marker${clarificationCount > 1 ? "s" : ""} — ADR is incomplete`,
259
+ });
260
+ }
261
+
262
+ // Check HLD sections (warning, not error — these are optional for simple ADRs)
263
+ const hasHldHeader = /^##\s+High-Level Design/im.test(content);
264
+ if (!hasHldHeader) {
265
+ findings.push({
266
+ severity: "warning",
267
+ message:
268
+ "Missing High-Level Design section — consider adding for complex decisions",
269
+ section: "High-Level Design",
270
+ });
271
+ } else {
272
+ for (const sub of HLD_SECTIONS) {
273
+ const escaped = sub.replace(/[&]/g, "\\$&");
274
+ const pattern = new RegExp(`^###\\s+${escaped}\\s*$`, "im");
275
+ if (!pattern.test(content)) {
276
+ findings.push({
277
+ severity: "warning",
278
+ message: `High-Level Design missing subsection: "${sub}"`,
279
+ section: `High-Level Design / ${sub}`,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ // Check LLD sections
286
+ const hasLldHeader = /^##\s+Low-Level Design/im.test(content);
287
+ if (!hasLldHeader) {
288
+ findings.push({
289
+ severity: "warning",
290
+ message:
291
+ "Missing Low-Level Design section — consider adding for complex decisions",
292
+ section: "Low-Level Design",
293
+ });
294
+ } else {
295
+ for (const sub of LLD_SECTIONS) {
296
+ const escaped = sub.replace(/[&]/g, "\\$&");
297
+ const pattern = new RegExp(`^###\\s+${escaped}\\s*$`, "im");
298
+ if (!pattern.test(content)) {
299
+ findings.push({
300
+ severity: "warning",
301
+ message: `Low-Level Design missing subsection: "${sub}"`,
302
+ section: `Low-Level Design / ${sub}`,
303
+ });
304
+ }
305
+ }
306
+ }
307
+
308
+ const hasErrors = findings.some((f) => f.severity === "error");
309
+
310
+ return {
311
+ ok: true,
312
+ value: {
313
+ adrPath: context.targetAdr.path,
314
+ findings,
315
+ passed: !hasErrors,
316
+ sectionsPresent: present,
317
+ sectionsMissing: missing,
318
+ },
319
+ };
320
+ } catch (e) {
321
+ const message = e instanceof Error ? e.message : String(e);
322
+ return {
323
+ ok: false,
324
+ error: `Review failed: ${message}`,
325
+ };
326
+ }
327
+ }