@mainahq/core 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mainahq/core",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Maina core engines — Context, Prompt, and Verify for verification-first development",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@ai-sdk/openai": "^3.0.50",
34
+ "@orama/orama": "^3.1.18",
34
35
  "ai": "^6.0.145",
35
36
  "drizzle-orm": "^0.45.2"
36
37
  },
@@ -137,7 +137,12 @@ describe("outputDelegationRequest", () => {
137
137
  process.stdout.write = originalStdoutWrite;
138
138
  });
139
139
 
140
- it("writes to stderr, not stdout (prevents MCP protocol corruption)", () => {
140
+ it("writes to stderr when inside AI tool, not stdout (prevents MCP corruption)", () => {
141
+ // Simulate running inside Claude Code
142
+ const origClaudeCode = process.env.CLAUDE_CODE;
143
+ process.env.CLAUDE_CODE = "1";
144
+ delete process.env.MAINA_MCP_SERVER;
145
+
141
146
  const req: DelegationRequest = {
142
147
  task: "review",
143
148
  context: "test context",
@@ -153,7 +158,46 @@ describe("outputDelegationRequest", () => {
153
158
  expect(stderrOutput).toContain("---MAINA_AI_REQUEST---");
154
159
  expect(stderrOutput).toContain("task: review");
155
160
 
156
- // Must NOT write to stdout (stdout is reserved for JSON-RPC in MCP)
161
+ // Must NOT write to stdout
162
+ expect(stdoutChunks.length).toBe(0);
163
+
164
+ // Restore
165
+ if (origClaudeCode) process.env.CLAUDE_CODE = origClaudeCode;
166
+ else delete process.env.CLAUDE_CODE;
167
+ });
168
+
169
+ it("suppresses output in MCP server mode", () => {
170
+ process.env.MAINA_MCP_SERVER = "1";
171
+ process.env.CLAUDE_CODE = "1";
172
+
173
+ outputDelegationRequest({
174
+ task: "test",
175
+ context: "ctx",
176
+ prompt: "p",
177
+ expectedFormat: "text",
178
+ });
179
+
180
+ expect(stderrChunks.length).toBe(0);
181
+ expect(stdoutChunks.length).toBe(0);
182
+
183
+ delete process.env.MAINA_MCP_SERVER;
184
+ delete process.env.CLAUDE_CODE;
185
+ });
186
+
187
+ it("suppresses output in bare terminal (no AI tool env vars)", () => {
188
+ delete process.env.CLAUDE_CODE;
189
+ delete process.env.CLAUDE_PROJECT_DIR;
190
+ delete process.env.CURSOR_TRACE_ID;
191
+ delete process.env.MAINA_MCP_SERVER;
192
+
193
+ outputDelegationRequest({
194
+ task: "test",
195
+ context: "ctx",
196
+ prompt: "p",
197
+ expectedFormat: "text",
198
+ });
199
+
200
+ expect(stderrChunks.length).toBe(0);
157
201
  expect(stdoutChunks.length).toBe(0);
158
202
  });
159
203
  });
@@ -103,11 +103,27 @@ export function parseDelegationRequest(text: string): DelegationRequest | null {
103
103
 
104
104
  /**
105
105
  * Output a delegation request to stderr.
106
- * Uses stderr so that MCP stdio transport (which uses stdout for JSON-RPC)
107
- * is never corrupted by delegation text.
108
- * In CLI mode, stderr is still visible in the terminal.
106
+ * Only outputs when running inside an AI tool that can process it
107
+ * (detected via CLAUDE_CODE, CURSOR, or similar env vars).
108
+ * Silent in bare terminal to avoid confusing users.
109
109
  */
110
110
  export function outputDelegationRequest(req: DelegationRequest): void {
111
+ // Never output in MCP mode — corrupts JSON-RPC communication
112
+ if (process.env.MAINA_MCP_SERVER === "1") {
113
+ return;
114
+ }
115
+
116
+ // Only output in bare CLI when inside an AI tool that can process it
117
+ const inAITool =
118
+ process.env.CLAUDE_CODE === "1" ||
119
+ process.env.CLAUDE_PROJECT_DIR ||
120
+ process.env.CURSOR_TRACE_ID ||
121
+ process.env.CONTINUE_GLOBAL_DIR;
122
+
123
+ if (!inAITool) {
124
+ return;
125
+ }
126
+
111
127
  const formatted = formatDelegationRequest(req);
112
128
  process.stderr.write(`\n${formatted}\n`);
113
129
  }
@@ -244,7 +244,17 @@ export function createCloudClient(config: CloudConfig): CloudClient {
244
244
 
245
245
  getPrompts: () => request<PromptRecord[]>("GET", "/prompts"),
246
246
 
247
- putPrompts: (prompts) => request<void>("PUT", "/prompts", { prompts }),
247
+ putPrompts: async (prompts) => {
248
+ for (const p of prompts) {
249
+ const result = await request<void>("PUT", "/prompts", {
250
+ name: p.id ?? p.path?.replace(/\.md$/, "") ?? "unknown",
251
+ content: p.content,
252
+ hash: p.hash,
253
+ });
254
+ if (!result.ok) return result;
255
+ }
256
+ return { ok: true as const, value: undefined };
257
+ },
248
258
 
249
259
  getTeam: () => request<TeamInfo>("GET", "/team"),
250
260
 
package/src/index.ts CHANGED
@@ -444,6 +444,15 @@ export {
444
444
  type CompileOptions as WikiCompileOptions,
445
445
  compile as compileWiki,
446
446
  } from "./wiki/compiler";
447
+ // Wiki — Consult
448
+ export {
449
+ consultWikiForBrainstorm,
450
+ consultWikiForDesign,
451
+ consultWikiForPlan,
452
+ type WikiBrainstormContext,
453
+ type WikiConsultResult,
454
+ type WikiDesignConsultResult,
455
+ } from "./wiki/consult";
447
456
  export { type CodeEntity, extractCodeEntities } from "./wiki/extractors/code";
448
457
  export {
449
458
  extractDecisions,
@@ -486,6 +495,15 @@ export {
486
495
  validateArticleStructure,
487
496
  type WikiSchema,
488
497
  } from "./wiki/schema";
498
+ // Wiki — Search
499
+ export {
500
+ buildSearchIndex,
501
+ loadSearchIndex,
502
+ saveSearchIndex,
503
+ searchWiki,
504
+ type WikiSearchIndex,
505
+ type WikiSearchResult,
506
+ } from "./wiki/search";
489
507
  // Wiki — Signals
490
508
  export {
491
509
  type ArticleLoadSignal,
package/src/init/index.ts CHANGED
@@ -1063,6 +1063,13 @@ async function tryGenerateConstitution(
1063
1063
  stack: DetectedStack,
1064
1064
  ): Promise<string | null> {
1065
1065
  try {
1066
+ // Only attempt AI generation if we have an actual API key.
1067
+ // Host delegation just dumps prompts to stderr which confuses users.
1068
+ const { getApiKey } = await import("../config/index");
1069
+ if (!getApiKey()) {
1070
+ return null;
1071
+ }
1072
+
1066
1073
  const { tryAIGenerate } = await import("../ai/try-generate");
1067
1074
  const mainaDir = join(repoRoot, ".maina");
1068
1075
  const summary = buildProjectSummary(repoRoot, stack);
@@ -7,6 +7,9 @@
7
7
  * Deterministic checks only — no AI calls.
8
8
  */
9
9
 
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
10
13
  // ── Types ────────────────────────────────────────────────────────────────────
11
14
 
12
15
  export interface ReviewStageResult {
@@ -28,6 +31,8 @@ export interface ReviewOptions {
28
31
  planContent?: string | null;
29
32
  conventions?: string | null;
30
33
  mainaDir?: string; // enables AI review when provided
34
+ /** Accepted ADR summaries for spec compliance checking */
35
+ decisionSummaries?: string[] | null;
31
36
  }
32
37
 
33
38
  export interface ReviewResult {
@@ -171,6 +176,7 @@ function extractAddedLines(
171
176
  export function reviewSpecCompliance(
172
177
  diff: string,
173
178
  planContent: string | null,
179
+ decisionSummaries?: string[] | null,
174
180
  ): ReviewStageResult {
175
181
  const findings: ReviewFinding[] = [];
176
182
 
@@ -229,6 +235,34 @@ export function reviewSpecCompliance(
229
235
  }
230
236
  }
231
237
 
238
+ // Check if changes align with accepted ADRs
239
+ if (decisionSummaries && decisionSummaries.length > 0) {
240
+ const addedLines = extractAddedLines(diff);
241
+ const addedText = addedLines.map((l) => l.text.toLowerCase()).join(" ");
242
+
243
+ for (const summary of decisionSummaries) {
244
+ // Extract tool/technology mentions from ADR summary
245
+ const summaryLower = summary.toLowerCase();
246
+ // Check for contradictions: if ADR mentions tool A but added code uses conflicting tool B
247
+ const knownConflicts: Array<[string, string]> = [
248
+ ["biome", "eslint"],
249
+ ["biome", "prettier"],
250
+ ["bun:test", "jest"],
251
+ ["bun:test", "vitest"],
252
+ ];
253
+
254
+ for (const [preferred, rejected] of knownConflicts) {
255
+ if (summaryLower.includes(preferred) && addedText.includes(rejected)) {
256
+ findings.push({
257
+ stage: "spec-compliance",
258
+ severity: "warning",
259
+ message: `ADR requires ${preferred} but added code references ${rejected}: "${summary.slice(0, 80)}"`,
260
+ });
261
+ }
262
+ }
263
+ }
264
+ }
265
+
232
266
  const hasWarningsOrErrors = findings.some(
233
267
  (f) => f.severity === "warning" || f.severity === "error",
234
268
  );
@@ -376,6 +410,51 @@ export async function reviewCodeQualityWithAI(
376
410
  return deterministicResult;
377
411
  }
378
412
 
413
+ // ── Decision Loader ─────────────────────────────────────────────────────────
414
+
415
+ /**
416
+ * Load accepted ADR summaries from the wiki decisions directory.
417
+ * Returns an array of one-line summaries for each accepted decision.
418
+ */
419
+ function loadDecisionSummaries(mainaDir: string): string[] | null {
420
+ const decisionsDir = join(mainaDir, "wiki", "decisions");
421
+ if (!existsSync(decisionsDir)) return null;
422
+
423
+ let entries: string[];
424
+ try {
425
+ entries = readdirSync(decisionsDir);
426
+ } catch {
427
+ return null;
428
+ }
429
+
430
+ const summaries: string[] = [];
431
+ for (const entry of entries) {
432
+ if (!entry.endsWith(".md")) continue;
433
+ try {
434
+ const content = readFileSync(join(decisionsDir, entry), "utf-8");
435
+ const statusMatch = content.match(/>\s*Status:\s*\*\*(\w+)\*\*/);
436
+ const status = statusMatch?.[1] ?? "";
437
+ if (status !== "accepted") continue;
438
+
439
+ const titleMatch = content.match(/^#\s+(.+)/);
440
+ const title =
441
+ titleMatch?.[1]?.replace(/^Decision:\s*/i, "").trim() ?? entry;
442
+
443
+ // Extract key constraint from decision section
444
+ const decisionMatch = content.match(
445
+ /## Decision\n\n([\s\S]*?)(?=\n## |\n---|$)/,
446
+ );
447
+ const decision = decisionMatch?.[1]?.trim().split("\n")[0] ?? "";
448
+
449
+ summaries.push(`${title}: ${decision}`);
450
+ } catch {
451
+ // skip unreadable files
452
+ }
453
+ }
454
+
455
+ return summaries.length > 0 ? summaries : null;
456
+ }
457
+
379
458
  // ── Two-Stage Review ────────────────────────────────────────────────────────
380
459
 
381
460
  /**
@@ -388,9 +467,16 @@ export async function reviewCodeQualityWithAI(
388
467
  export async function runTwoStageReview(
389
468
  options: ReviewOptions,
390
469
  ): Promise<ReviewResult> {
470
+ // Load decision summaries from wiki if mainaDir is provided
471
+ let decisionSummaries = options.decisionSummaries ?? null;
472
+ if (!decisionSummaries && options.mainaDir) {
473
+ decisionSummaries = loadDecisionSummaries(options.mainaDir);
474
+ }
475
+
391
476
  const stage1 = reviewSpecCompliance(
392
477
  options.diff,
393
478
  options.planContent ?? null,
479
+ decisionSummaries,
394
480
  );
395
481
 
396
482
  if (!stage1.passed) {
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Tests for wiki consult — pre-command wiki consultation.
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import {
9
+ consultWikiForBrainstorm,
10
+ consultWikiForDesign,
11
+ consultWikiForPlan,
12
+ } from "../consult";
13
+
14
+ // ── Test Helpers ────────────────────────────────────────────────────────────
15
+
16
+ let tmpDir: string;
17
+ let wikiDir: string;
18
+
19
+ function createTmpDir(): string {
20
+ const dir = join(
21
+ import.meta.dir,
22
+ `tmp-consult-${Date.now()}-${Math.random().toString(36).slice(2)}`,
23
+ );
24
+ mkdirSync(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ function seedWiki(wiki: string): void {
29
+ const subdirs = [
30
+ "modules",
31
+ "entities",
32
+ "features",
33
+ "decisions",
34
+ "architecture",
35
+ ];
36
+ for (const subdir of subdirs) {
37
+ mkdirSync(join(wiki, subdir), { recursive: true });
38
+ }
39
+
40
+ // Module: verify with 11 entities
41
+ writeFileSync(
42
+ join(wiki, "modules", "verify.md"),
43
+ [
44
+ "# Module: verify",
45
+ "",
46
+ "> Auto-generated module article for `verify`.",
47
+ "",
48
+ "## Entities",
49
+ "",
50
+ "- **runPipeline** (function) — `verify/pipeline.ts:10`",
51
+ "- **syntaxGuard** (function) — `verify/syntax.ts:5`",
52
+ "- **diffFilter** (function) — `verify/diff.ts:1`",
53
+ "- **slopDetector** (function) — `verify/slop.ts:8`",
54
+ "- **reviewCode** (function) — `verify/review.ts:3`",
55
+ "- **PipelineResult** (interface) — `verify/types.ts:1`",
56
+ "- **Finding** (interface) — `verify/types.ts:20`",
57
+ "- **ToolRunner** (interface) — `verify/types.ts:40`",
58
+ "- **SyntaxError** (interface) — `verify/types.ts:60`",
59
+ "- **DiffFilter** (interface) — `verify/types.ts:80`",
60
+ "- **SlopPattern** (interface) — `verify/types.ts:100`",
61
+ ].join("\n"),
62
+ );
63
+
64
+ // Module: context with 8 entities
65
+ writeFileSync(
66
+ join(wiki, "modules", "context.md"),
67
+ [
68
+ "# Module: context",
69
+ "",
70
+ "> Auto-generated module article for `context`.",
71
+ "",
72
+ "## Entities",
73
+ "",
74
+ "- **assembleContext** (function) — `context/engine.ts:10`",
75
+ "- **parseFile** (function) — `context/treesitter.ts:5`",
76
+ "- **extractEntities** (function) — `context/treesitter.ts:50`",
77
+ "- **ContextResult** (interface) — `context/types.ts:1`",
78
+ "- **TokenBudget** (interface) — `context/budget.ts:1`",
79
+ "- **SemanticLayer** (class) — `context/semantic.ts:10`",
80
+ "- **EpisodicLayer** (class) — `context/episodic.ts:10`",
81
+ "- **WorkingLayer** (class) — `context/working.ts:10`",
82
+ ].join("\n"),
83
+ );
84
+
85
+ // Decision: accepted — multi-language with Biome
86
+ writeFileSync(
87
+ join(wiki, "decisions", "0002-multi-language-verify-pipeline.md"),
88
+ [
89
+ "# Decision: Multi-language verify pipeline",
90
+ "",
91
+ "> Status: **accepted**",
92
+ "",
93
+ "## Context",
94
+ "",
95
+ "Maina needs multi-language support for the verify pipeline.",
96
+ "",
97
+ "## Decision",
98
+ "",
99
+ "Use Biome for TypeScript linting. Use ruff for Python. Use clippy for Rust.",
100
+ "Introduce LanguageProfile abstraction for each supported language.",
101
+ ].join("\n"),
102
+ );
103
+
104
+ // Decision: proposed — spec quality
105
+ writeFileSync(
106
+ join(wiki, "decisions", "0001-spec-quality.md"),
107
+ [
108
+ "# Decision: Spec quality system",
109
+ "",
110
+ "> Status: **proposed**",
111
+ "",
112
+ "## Context",
113
+ "",
114
+ "Specifications need quality scoring to prevent bad specs.",
115
+ "",
116
+ "## Decision",
117
+ "",
118
+ "Build a spec scoring system with measurability and testability checks.",
119
+ ].join("\n"),
120
+ );
121
+
122
+ // Feature: similar to hardening
123
+ writeFileSync(
124
+ join(wiki, "features", "024-v03x-hardening.md"),
125
+ [
126
+ "# Feature: Implementation Plan — v0.3.x Hardening",
127
+ "",
128
+ "## Status",
129
+ "",
130
+ "Verify pipeline improvements, gap fixes, RL loop integration.",
131
+ ].join("\n"),
132
+ );
133
+
134
+ // Feature: benchmark
135
+ writeFileSync(
136
+ join(wiki, "features", "010-benchmark-harness.md"),
137
+ [
138
+ "# Feature: Benchmark harness",
139
+ "",
140
+ "## Status",
141
+ "",
142
+ "Full lifecycle benchmark comparison framework.",
143
+ ].join("\n"),
144
+ );
145
+
146
+ // Architecture
147
+ writeFileSync(
148
+ join(wiki, "architecture", "three-engines.md"),
149
+ [
150
+ "# Architecture: Three Engines",
151
+ "",
152
+ "Maina uses three engines: Context, Prompt, and Verify.",
153
+ "Context observes, Prompt learns, Verify verifies.",
154
+ ].join("\n"),
155
+ );
156
+
157
+ writeFileSync(
158
+ join(wiki, "architecture", "verification-pipeline.md"),
159
+ [
160
+ "# Architecture: Verification Pipeline",
161
+ "",
162
+ "The verify pipeline has multiple stages: syntax guard, parallel tools, diff filter, AI fix, review.",
163
+ ].join("\n"),
164
+ );
165
+ }
166
+
167
+ beforeEach(() => {
168
+ tmpDir = createTmpDir();
169
+ wikiDir = join(tmpDir, "wiki");
170
+ });
171
+
172
+ afterEach(() => {
173
+ try {
174
+ rmSync(tmpDir, { recursive: true, force: true });
175
+ } catch {
176
+ // ignore
177
+ }
178
+ });
179
+
180
+ // ── consultWikiForPlan ─────────────────────────────────────────────────────
181
+
182
+ describe("consultWikiForPlan", () => {
183
+ test("finds related modules by keyword", () => {
184
+ seedWiki(wikiDir);
185
+ const result = consultWikiForPlan(
186
+ wikiDir,
187
+ "improve the verify pipeline syntax checking",
188
+ );
189
+
190
+ expect(result.relatedModules.length).toBeGreaterThan(0);
191
+ const verifyMod = result.relatedModules.find((m) => m.name === "verify");
192
+ expect(verifyMod).toBeDefined();
193
+ expect(verifyMod?.entities).toBe(11);
194
+ });
195
+
196
+ test("returns suggestions for modules with many entities", () => {
197
+ seedWiki(wikiDir);
198
+ const result = consultWikiForPlan(
199
+ wikiDir,
200
+ "improve the verify pipeline syntax checking",
201
+ );
202
+
203
+ const extendSuggestion = result.suggestions.find((s) =>
204
+ s.includes("consider extending"),
205
+ );
206
+ expect(extendSuggestion).toBeDefined();
207
+ expect(extendSuggestion).toContain("verify");
208
+ });
209
+
210
+ test("finds related decisions", () => {
211
+ seedWiki(wikiDir);
212
+ const result = consultWikiForPlan(
213
+ wikiDir,
214
+ "add multi-language linting support",
215
+ );
216
+
217
+ expect(result.relatedDecisions.length).toBeGreaterThan(0);
218
+ const multiLang = result.relatedDecisions.find(
219
+ (d) => d.id === "0002-multi-language-verify-pipeline",
220
+ );
221
+ expect(multiLang).toBeDefined();
222
+ expect(multiLang?.status).toBe("accepted");
223
+ });
224
+
225
+ test("finds related features", () => {
226
+ seedWiki(wikiDir);
227
+ const result = consultWikiForPlan(
228
+ wikiDir,
229
+ "hardening improvements for verify gaps",
230
+ );
231
+
232
+ expect(result.relatedFeatures.length).toBeGreaterThan(0);
233
+ const hardening = result.relatedFeatures.find((f) => f.id.includes("024"));
234
+ expect(hardening).toBeDefined();
235
+ });
236
+
237
+ test("generates feature similarity suggestions", () => {
238
+ seedWiki(wikiDir);
239
+ const result = consultWikiForPlan(
240
+ wikiDir,
241
+ "hardening improvements for verify gaps",
242
+ );
243
+
244
+ const featureSuggestion = result.suggestions.find((s) =>
245
+ s.includes("did something similar"),
246
+ );
247
+ expect(featureSuggestion).toBeDefined();
248
+ });
249
+
250
+ test("handles missing wiki gracefully", () => {
251
+ const result = consultWikiForPlan("/nonexistent/wiki", "some feature");
252
+ expect(result.relatedModules).toEqual([]);
253
+ expect(result.relatedDecisions).toEqual([]);
254
+ expect(result.relatedFeatures).toEqual([]);
255
+ expect(result.suggestions).toEqual([]);
256
+ });
257
+
258
+ test("handles empty description", () => {
259
+ seedWiki(wikiDir);
260
+ const result = consultWikiForPlan(wikiDir, "");
261
+ expect(result.relatedModules).toEqual([]);
262
+ });
263
+ });
264
+
265
+ // ── consultWikiForDesign ───────────────────────────────────────────────────
266
+
267
+ describe("consultWikiForDesign", () => {
268
+ test("detects conflicts with existing ADRs", () => {
269
+ seedWiki(wikiDir);
270
+ const result = consultWikiForDesign(
271
+ wikiDir,
272
+ "Use ESLint for linting TypeScript code instead of current tools",
273
+ );
274
+
275
+ expect(result.conflicts.length).toBeGreaterThan(0);
276
+ const biomeConflict = result.conflicts.find((c) =>
277
+ c.reason.includes("biome"),
278
+ );
279
+ expect(biomeConflict).toBeDefined();
280
+ });
281
+
282
+ test("detects alignments with existing ADRs", () => {
283
+ seedWiki(wikiDir);
284
+ const result = consultWikiForDesign(
285
+ wikiDir,
286
+ "Extend the verify pipeline to support multi-language linting with ruff",
287
+ );
288
+
289
+ expect(result.alignments.length).toBeGreaterThan(0);
290
+ });
291
+
292
+ test("handles missing wiki gracefully", () => {
293
+ const result = consultWikiForDesign("/nonexistent/wiki", "some decision");
294
+ expect(result.conflicts).toEqual([]);
295
+ expect(result.alignments).toEqual([]);
296
+ });
297
+
298
+ test("handles empty proposal", () => {
299
+ seedWiki(wikiDir);
300
+ const result = consultWikiForDesign(wikiDir, "");
301
+ expect(result.conflicts).toEqual([]);
302
+ expect(result.alignments).toEqual([]);
303
+ });
304
+ });
305
+
306
+ // ── consultWikiForBrainstorm ───────────────────────────────────────────────
307
+
308
+ describe("consultWikiForBrainstorm", () => {
309
+ test("loads architecture context", () => {
310
+ seedWiki(wikiDir);
311
+ const result = consultWikiForBrainstorm(wikiDir);
312
+
313
+ expect(result.architecture).toContain("Three Engines");
314
+ expect(result.architecture).toContain("Verification Pipeline");
315
+ });
316
+
317
+ test("counts modules and decisions", () => {
318
+ seedWiki(wikiDir);
319
+ const result = consultWikiForBrainstorm(wikiDir);
320
+
321
+ expect(result.moduleCount).toBe(2);
322
+ expect(result.decisionCount).toBe(2);
323
+ });
324
+
325
+ test("loads recent features", () => {
326
+ seedWiki(wikiDir);
327
+ const result = consultWikiForBrainstorm(wikiDir);
328
+
329
+ expect(result.recentFeatures.length).toBe(2);
330
+ // Sorted descending by ID, so 024 comes first
331
+ expect(result.recentFeatures[0]).toContain("024");
332
+ });
333
+
334
+ test("handles missing wiki gracefully", () => {
335
+ const result = consultWikiForBrainstorm("/nonexistent/wiki");
336
+ expect(result.architecture).toBe("");
337
+ expect(result.moduleCount).toBe(0);
338
+ expect(result.decisionCount).toBe(0);
339
+ expect(result.recentFeatures).toEqual([]);
340
+ });
341
+ });