@mainahq/core 1.1.1 → 1.1.2
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 +2 -1
- package/src/ai/delegation.ts +14 -3
- package/src/cloud/client.ts +11 -1
- package/src/index.ts +18 -0
- package/src/init/index.ts +7 -0
- package/src/review/index.ts +86 -0
- package/src/wiki/__tests__/consult.test.ts +341 -0
- package/src/wiki/__tests__/search.test.ts +384 -0
- package/src/wiki/compiler.ts +11 -0
- package/src/wiki/consult.ts +395 -0
- package/src/wiki/query.ts +28 -2
- package/src/wiki/search.ts +346 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mainahq/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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
|
},
|
package/src/ai/delegation.ts
CHANGED
|
@@ -103,11 +103,22 @@ export function parseDelegationRequest(text: string): DelegationRequest | null {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Output a delegation request to stderr.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
+
// Only output delegation when inside an AI tool that can process it
|
|
112
|
+
const inAITool =
|
|
113
|
+
process.env.CLAUDE_CODE === "1" ||
|
|
114
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
115
|
+
process.env.CURSOR_TRACE_ID ||
|
|
116
|
+
process.env.CONTINUE_GLOBAL_DIR;
|
|
117
|
+
|
|
118
|
+
if (!inAITool) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
const formatted = formatDelegationRequest(req);
|
|
112
123
|
process.stderr.write(`\n${formatted}\n`);
|
|
113
124
|
}
|
package/src/cloud/client.ts
CHANGED
|
@@ -244,7 +244,17 @@ export function createCloudClient(config: CloudConfig): CloudClient {
|
|
|
244
244
|
|
|
245
245
|
getPrompts: () => request<PromptRecord[]>("GET", "/prompts"),
|
|
246
246
|
|
|
247
|
-
putPrompts: (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);
|
package/src/review/index.ts
CHANGED
|
@@ -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
|
+
});
|