@mainahq/core 1.0.3 → 1.1.1

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 (71) hide show
  1. package/package.json +1 -1
  2. package/src/ai/__tests__/delegation.test.ts +55 -1
  3. package/src/ai/delegation.ts +5 -3
  4. package/src/context/__tests__/budget.test.ts +29 -6
  5. package/src/context/__tests__/engine.test.ts +1 -0
  6. package/src/context/__tests__/selector.test.ts +23 -3
  7. package/src/context/__tests__/wiki.test.ts +349 -0
  8. package/src/context/budget.ts +12 -8
  9. package/src/context/engine.ts +37 -0
  10. package/src/context/selector.ts +30 -4
  11. package/src/context/wiki.ts +296 -0
  12. package/src/db/index.ts +12 -0
  13. package/src/feedback/__tests__/capture.test.ts +166 -0
  14. package/src/feedback/__tests__/signals.test.ts +144 -0
  15. package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
  16. package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
  17. package/src/feedback/capture.ts +102 -0
  18. package/src/feedback/signals.ts +68 -0
  19. package/src/index.ts +104 -0
  20. package/src/init/__tests__/init.test.ts +400 -3
  21. package/src/init/index.ts +368 -12
  22. package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
  23. package/src/prompts/defaults/index.ts +3 -1
  24. package/src/prompts/defaults/wiki-compile.md +20 -0
  25. package/src/prompts/defaults/wiki-query.md +18 -0
  26. package/src/stats/__tests__/tool-usage.test.ts +133 -0
  27. package/src/stats/tracker.ts +92 -0
  28. package/src/verify/__tests__/pipeline.test.ts +11 -8
  29. package/src/verify/pipeline.ts +13 -1
  30. package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
  31. package/src/verify/tools/wiki-lint-runner.ts +38 -0
  32. package/src/verify/tools/wiki-lint.ts +898 -0
  33. package/src/wiki/__tests__/compiler.test.ts +389 -0
  34. package/src/wiki/__tests__/extractors/code.test.ts +99 -0
  35. package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
  36. package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
  37. package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
  38. package/src/wiki/__tests__/graph.test.ts +344 -0
  39. package/src/wiki/__tests__/hooks.test.ts +119 -0
  40. package/src/wiki/__tests__/indexer.test.ts +285 -0
  41. package/src/wiki/__tests__/linker.test.ts +230 -0
  42. package/src/wiki/__tests__/louvain.test.ts +229 -0
  43. package/src/wiki/__tests__/query.test.ts +316 -0
  44. package/src/wiki/__tests__/schema.test.ts +114 -0
  45. package/src/wiki/__tests__/signals.test.ts +474 -0
  46. package/src/wiki/__tests__/state.test.ts +168 -0
  47. package/src/wiki/__tests__/tracking.test.ts +118 -0
  48. package/src/wiki/__tests__/types.test.ts +387 -0
  49. package/src/wiki/compiler.ts +1075 -0
  50. package/src/wiki/extractors/code.ts +90 -0
  51. package/src/wiki/extractors/decision.ts +217 -0
  52. package/src/wiki/extractors/feature.ts +206 -0
  53. package/src/wiki/extractors/workflow.ts +112 -0
  54. package/src/wiki/graph.ts +445 -0
  55. package/src/wiki/hooks.ts +49 -0
  56. package/src/wiki/indexer.ts +105 -0
  57. package/src/wiki/linker.ts +117 -0
  58. package/src/wiki/louvain.ts +190 -0
  59. package/src/wiki/prompts/compile-architecture.md +59 -0
  60. package/src/wiki/prompts/compile-decision.md +66 -0
  61. package/src/wiki/prompts/compile-entity.md +56 -0
  62. package/src/wiki/prompts/compile-feature.md +60 -0
  63. package/src/wiki/prompts/compile-module.md +42 -0
  64. package/src/wiki/prompts/wiki-query.md +25 -0
  65. package/src/wiki/query.ts +338 -0
  66. package/src/wiki/schema.ts +111 -0
  67. package/src/wiki/signals.ts +368 -0
  68. package/src/wiki/state.ts +89 -0
  69. package/src/wiki/tracking.ts +30 -0
  70. package/src/wiki/types.ts +169 -0
  71. package/src/workflow/context.ts +26 -0
@@ -0,0 +1,389 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import type { CompileOptions } from "../compiler";
12
+ import { compile } from "../compiler";
13
+
14
+ // ─── Test Fixtures ──────────────────────────────────────────────────────
15
+
16
+ let tmpDir: string;
17
+ let repoRoot: string;
18
+ let mainaDir: string;
19
+ let wikiDir: string;
20
+
21
+ function makeOptions(overrides?: Partial<CompileOptions>): CompileOptions {
22
+ return {
23
+ repoRoot,
24
+ mainaDir,
25
+ wikiDir,
26
+ full: true,
27
+ dryRun: false,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ beforeEach(() => {
33
+ tmpDir = join(
34
+ tmpdir(),
35
+ `wiki-compiler-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
36
+ );
37
+ repoRoot = join(tmpDir, "repo");
38
+ mainaDir = join(repoRoot, ".maina");
39
+ wikiDir = join(mainaDir, "wiki");
40
+
41
+ // Create repo structure
42
+ mkdirSync(join(repoRoot, "src", "auth"), { recursive: true });
43
+ mkdirSync(join(repoRoot, "src", "cache"), { recursive: true });
44
+ mkdirSync(join(mainaDir, "features", "001-auth"), { recursive: true });
45
+ mkdirSync(join(repoRoot, "adr"), { recursive: true });
46
+ mkdirSync(wikiDir, { recursive: true });
47
+
48
+ // Write source files
49
+ writeFileSync(
50
+ join(repoRoot, "src", "auth", "jwt.ts"),
51
+ [
52
+ "export function signToken(payload: unknown): string {",
53
+ ' return "token";',
54
+ "}",
55
+ "",
56
+ "export function verifyToken(token: string): boolean {",
57
+ " return true;",
58
+ "}",
59
+ "",
60
+ "export interface AuthConfig {",
61
+ " secret: string;",
62
+ " expiresIn: number;",
63
+ "}",
64
+ ].join("\n"),
65
+ );
66
+
67
+ writeFileSync(
68
+ join(repoRoot, "src", "cache", "manager.ts"),
69
+ [
70
+ "export class CacheManager {",
71
+ " get(key: string): unknown { return null; }",
72
+ " set(key: string, value: unknown): void {}",
73
+ "}",
74
+ "",
75
+ "export function createCache(): CacheManager {",
76
+ " return new CacheManager();",
77
+ "}",
78
+ ].join("\n"),
79
+ );
80
+
81
+ // Write feature files
82
+ writeFileSync(
83
+ join(mainaDir, "features", "001-auth", "plan.md"),
84
+ [
85
+ "# Implementation Plan: JWT Authentication",
86
+ "",
87
+ "## Tasks",
88
+ "- [x] T001: Implement signToken",
89
+ "- [ ] T002: Implement verifyToken",
90
+ ].join("\n"),
91
+ );
92
+
93
+ writeFileSync(
94
+ join(mainaDir, "features", "001-auth", "spec.md"),
95
+ [
96
+ "# Feature: JWT Authentication",
97
+ "",
98
+ "## Scope",
99
+ "Add JWT-based authentication to the API.",
100
+ "",
101
+ "## Acceptance Criteria",
102
+ "- [ ] Tokens expire after 1 hour",
103
+ "- [ ] Tokens contain user ID",
104
+ ].join("\n"),
105
+ );
106
+
107
+ // Write ADR file
108
+ writeFileSync(
109
+ join(repoRoot, "adr", "0001-use-jwt.md"),
110
+ [
111
+ "# ADR-0001: Use JWT for Authentication",
112
+ "",
113
+ "## Status",
114
+ "Accepted",
115
+ "",
116
+ "## Context",
117
+ "We need stateless authentication for our API.",
118
+ "",
119
+ "## Decision",
120
+ "Use JWT tokens with RS256 signing.",
121
+ "",
122
+ "## Rationale",
123
+ "JWTs are stateless and work well with microservices.",
124
+ "",
125
+ "## Alternatives Considered",
126
+ "- Session-based auth",
127
+ "- OAuth2 only",
128
+ ].join("\n"),
129
+ );
130
+ });
131
+
132
+ afterEach(() => {
133
+ rmSync(tmpDir, { recursive: true, force: true });
134
+ });
135
+
136
+ // ─── Tests ──────────────────────────────────────────────────────────────
137
+
138
+ describe("Wiki Compiler", () => {
139
+ describe("compile", () => {
140
+ it("should return ok result on successful compilation", async () => {
141
+ const result = await compile(makeOptions());
142
+ expect(result.ok).toBe(true);
143
+ });
144
+
145
+ it("should produce articles array", async () => {
146
+ const result = await compile(makeOptions());
147
+ expect(result.ok).toBe(true);
148
+ if (!result.ok) return;
149
+
150
+ expect(result.value.articles.length).toBeGreaterThan(0);
151
+ });
152
+
153
+ it("should produce module articles", async () => {
154
+ const result = await compile(makeOptions());
155
+ expect(result.ok).toBe(true);
156
+ if (!result.ok) return;
157
+
158
+ const moduleArticles = result.value.articles.filter(
159
+ (a) => a.type === "module",
160
+ );
161
+ expect(moduleArticles.length).toBeGreaterThan(0);
162
+ });
163
+
164
+ it("should produce entity articles for top entities", async () => {
165
+ const result = await compile(makeOptions());
166
+ expect(result.ok).toBe(true);
167
+ if (!result.ok) return;
168
+
169
+ const entityArticles = result.value.articles.filter(
170
+ (a) => a.type === "entity",
171
+ );
172
+ expect(entityArticles.length).toBeGreaterThan(0);
173
+ });
174
+
175
+ it("should produce feature articles", async () => {
176
+ const result = await compile(makeOptions());
177
+ expect(result.ok).toBe(true);
178
+ if (!result.ok) return;
179
+
180
+ const featureArticles = result.value.articles.filter(
181
+ (a) => a.type === "feature",
182
+ );
183
+ expect(featureArticles.length).toBe(1);
184
+ expect(featureArticles[0]?.title).toContain("JWT Authentication");
185
+ });
186
+
187
+ it("should produce decision articles", async () => {
188
+ const result = await compile(makeOptions());
189
+ expect(result.ok).toBe(true);
190
+ if (!result.ok) return;
191
+
192
+ const decisionArticles = result.value.articles.filter(
193
+ (a) => a.type === "decision",
194
+ );
195
+ expect(decisionArticles.length).toBe(1);
196
+ });
197
+
198
+ it("should produce an index article", async () => {
199
+ const result = await compile(makeOptions());
200
+ expect(result.ok).toBe(true);
201
+ if (!result.ok) return;
202
+
203
+ const indexArticle = result.value.articles.find(
204
+ (a) => a.path === "wiki/index.md",
205
+ );
206
+ expect(indexArticle).toBeDefined();
207
+ expect(indexArticle?.content).toContain("# Wiki Index");
208
+ });
209
+
210
+ it("should write articles to disk", async () => {
211
+ const result = await compile(makeOptions());
212
+ expect(result.ok).toBe(true);
213
+ if (!result.ok) return;
214
+
215
+ // Check that index.md was written
216
+ const indexPath = join(wikiDir, "index.md");
217
+ expect(existsSync(indexPath)).toBe(true);
218
+
219
+ const content = readFileSync(indexPath, "utf-8");
220
+ expect(content).toContain("# Wiki Index");
221
+ });
222
+
223
+ it("should create directory structure on disk", async () => {
224
+ const result = await compile(makeOptions());
225
+ expect(result.ok).toBe(true);
226
+ if (!result.ok) return;
227
+
228
+ // Module directory should exist
229
+ expect(existsSync(join(wikiDir, "modules"))).toBe(true);
230
+ });
231
+
232
+ it("should save state after compilation", async () => {
233
+ const result = await compile(makeOptions());
234
+ expect(result.ok).toBe(true);
235
+ if (!result.ok) return;
236
+
237
+ const statePath = join(wikiDir, ".state.json");
238
+ expect(existsSync(statePath)).toBe(true);
239
+
240
+ const state = JSON.parse(readFileSync(statePath, "utf-8"));
241
+ expect(state.lastFullCompile).toBeTruthy();
242
+ expect(Object.keys(state.articleHashes).length).toBeGreaterThan(0);
243
+ });
244
+
245
+ it("should not write to disk in dry run mode", async () => {
246
+ // Remove the wiki dir first to ensure it's clean
247
+ rmSync(wikiDir, { recursive: true, force: true });
248
+ mkdirSync(wikiDir, { recursive: true });
249
+
250
+ const result = await compile(makeOptions({ dryRun: true }));
251
+ expect(result.ok).toBe(true);
252
+ if (!result.ok) return;
253
+
254
+ // index.md should not be written
255
+ expect(existsSync(join(wikiDir, "index.md"))).toBe(false);
256
+ // State should not be saved
257
+ expect(existsSync(join(wikiDir, ".state.json"))).toBe(false);
258
+ });
259
+
260
+ it("should return compilation stats", async () => {
261
+ const result = await compile(makeOptions());
262
+ expect(result.ok).toBe(true);
263
+ if (!result.ok) return;
264
+
265
+ expect(result.value.stats.modules).toBeGreaterThanOrEqual(0);
266
+ expect(result.value.stats.entities).toBeGreaterThanOrEqual(0);
267
+ expect(result.value.stats.features).toBe(1);
268
+ expect(result.value.stats.decisions).toBe(1);
269
+ // Index article counts as architecture
270
+ expect(result.value.stats.architecture).toBe(1);
271
+ });
272
+
273
+ it("should return duration in milliseconds", async () => {
274
+ const result = await compile(makeOptions());
275
+ expect(result.ok).toBe(true);
276
+ if (!result.ok) return;
277
+
278
+ expect(result.value.duration).toBeGreaterThanOrEqual(0);
279
+ });
280
+
281
+ it("should return the knowledge graph", async () => {
282
+ const result = await compile(makeOptions());
283
+ expect(result.ok).toBe(true);
284
+ if (!result.ok) return;
285
+
286
+ expect(result.value.graph.nodes.size).toBeGreaterThan(0);
287
+ expect(result.value.graph.edges.length).toBeGreaterThan(0);
288
+ });
289
+
290
+ it("should set content hashes on all articles", async () => {
291
+ const result = await compile(makeOptions());
292
+ expect(result.ok).toBe(true);
293
+ if (!result.ok) return;
294
+
295
+ for (const article of result.value.articles) {
296
+ expect(article.contentHash).toBeTruthy();
297
+ expect(article.contentHash).toMatch(/^[a-f0-9]{64}$/);
298
+ }
299
+ });
300
+
301
+ it("should set lastCompiled timestamp on all articles", async () => {
302
+ const result = await compile(makeOptions());
303
+ expect(result.ok).toBe(true);
304
+ if (!result.ok) return;
305
+
306
+ for (const article of result.value.articles) {
307
+ expect(article.lastCompiled).toBeTruthy();
308
+ // Should be a valid ISO date
309
+ expect(Number.isNaN(new Date(article.lastCompiled).getTime())).toBe(
310
+ false,
311
+ );
312
+ }
313
+ });
314
+
315
+ it("should handle empty repo gracefully", async () => {
316
+ // Create an empty repo
317
+ const emptyRepo = join(tmpDir, "empty-repo");
318
+ const emptyMaina = join(emptyRepo, ".maina");
319
+ const emptyWiki = join(emptyMaina, "wiki");
320
+ mkdirSync(emptyWiki, { recursive: true });
321
+
322
+ const result = await compile({
323
+ repoRoot: emptyRepo,
324
+ mainaDir: emptyMaina,
325
+ wikiDir: emptyWiki,
326
+ full: true,
327
+ });
328
+
329
+ expect(result.ok).toBe(true);
330
+ if (!result.ok) return;
331
+
332
+ // Should still produce an index article
333
+ const indexArticle = result.value.articles.find(
334
+ (a) => a.path === "wiki/index.md",
335
+ );
336
+ expect(indexArticle).toBeDefined();
337
+ });
338
+
339
+ it("should generate article content as valid markdown", async () => {
340
+ const result = await compile(makeOptions());
341
+ expect(result.ok).toBe(true);
342
+ if (!result.ok) return;
343
+
344
+ for (const article of result.value.articles) {
345
+ // Every article should start with a markdown heading
346
+ expect(article.content.trimStart().startsWith("#")).toBe(true);
347
+ }
348
+ });
349
+
350
+ it("should pass useAI flag through without crashing", async () => {
351
+ // useAI: true should be accepted — AI will silently fall back since no key is set
352
+ const result = await compile(makeOptions({ useAI: true }));
353
+ expect(result.ok).toBe(true);
354
+ if (!result.ok) return;
355
+
356
+ expect(result.value.articles.length).toBeGreaterThan(0);
357
+ });
358
+
359
+ it("should compile normally when useAI is false", async () => {
360
+ const result = await compile(makeOptions({ useAI: false }));
361
+ expect(result.ok).toBe(true);
362
+ if (!result.ok) return;
363
+
364
+ expect(result.value.articles.length).toBeGreaterThan(0);
365
+ });
366
+
367
+ it("should skip test files and .d.ts files during extraction", async () => {
368
+ // Create a test file and a .d.ts file
369
+ writeFileSync(
370
+ join(repoRoot, "src", "auth", "jwt.test.ts"),
371
+ "export function testHelper(): void {}",
372
+ );
373
+ writeFileSync(
374
+ join(repoRoot, "src", "auth", "jwt.d.ts"),
375
+ "export declare function signToken(): string;",
376
+ );
377
+
378
+ const result = await compile(makeOptions());
379
+ expect(result.ok).toBe(true);
380
+ if (!result.ok) return;
381
+
382
+ // Test helper should not appear as an entity
383
+ const hasTestHelper = result.value.articles.some((a) =>
384
+ a.content.includes("testHelper"),
385
+ );
386
+ expect(hasTestHelper).toBe(false);
387
+ });
388
+ });
389
+ });
@@ -0,0 +1,99 @@
1
+ import { afterEach, beforeEach, describe, expect, it } 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 { extractCodeEntities } from "../../extractors/code";
6
+
7
+ let tmpDir: string;
8
+
9
+ beforeEach(() => {
10
+ tmpDir = join(
11
+ tmpdir(),
12
+ `wiki-code-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
13
+ );
14
+ mkdirSync(join(tmpDir, "src"), { recursive: true });
15
+ });
16
+
17
+ afterEach(() => {
18
+ rmSync(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ describe("Code Entity Extractor", () => {
22
+ describe("extractCodeEntities", () => {
23
+ it("happy path: should extract entities from TypeScript files", () => {
24
+ writeFileSync(
25
+ join(tmpDir, "src", "math.ts"),
26
+ [
27
+ "export function add(a: number, b: number): number {",
28
+ " return a + b;",
29
+ "}",
30
+ "",
31
+ "export function multiply(a: number, b: number): number {",
32
+ " return a * b;",
33
+ "}",
34
+ "",
35
+ "export interface Calculator {",
36
+ " compute(expr: string): number;",
37
+ "}",
38
+ ].join("\n"),
39
+ );
40
+
41
+ const result = extractCodeEntities(tmpDir, ["src/math.ts"]);
42
+ expect(result.ok).toBe(true);
43
+ if (!result.ok) return;
44
+
45
+ // Should find exported functions and interfaces
46
+ expect(result.value.length).toBeGreaterThan(0);
47
+ const names = result.value.map((e) => e.name);
48
+ expect(names).toContain("add");
49
+ expect(names).toContain("multiply");
50
+ expect(names).toContain("Calculator");
51
+ });
52
+
53
+ it("should include file path and entity kind", () => {
54
+ writeFileSync(
55
+ join(tmpDir, "src", "types.ts"),
56
+ [
57
+ "export type Status = 'active' | 'inactive';",
58
+ "",
59
+ "export const VERSION = '1.0.0';",
60
+ ].join("\n"),
61
+ );
62
+
63
+ const result = extractCodeEntities(tmpDir, ["src/types.ts"]);
64
+ expect(result.ok).toBe(true);
65
+ if (!result.ok) return;
66
+
67
+ for (const entity of result.value) {
68
+ expect(entity.file).toBeTruthy();
69
+ expect(entity.kind).toBeTruthy();
70
+ expect(entity.name).toBeTruthy();
71
+ }
72
+ });
73
+
74
+ it("should handle empty file list", () => {
75
+ const result = extractCodeEntities(tmpDir, []);
76
+ expect(result.ok).toBe(true);
77
+ if (!result.ok) return;
78
+ expect(result.value).toHaveLength(0);
79
+ });
80
+
81
+ it("should handle non-existent files gracefully", () => {
82
+ const result = extractCodeEntities(tmpDir, ["src/nonexistent.ts"]);
83
+ expect(result.ok).toBe(true);
84
+ if (!result.ok) return;
85
+ expect(result.value).toHaveLength(0);
86
+ });
87
+
88
+ it("edge case: file with no exports", () => {
89
+ writeFileSync(
90
+ join(tmpDir, "src", "internal.ts"),
91
+ "const secret = 42;\nfunction helper() { return secret; }\n",
92
+ );
93
+
94
+ const result = extractCodeEntities(tmpDir, ["src/internal.ts"]);
95
+ expect(result.ok).toBe(true);
96
+ // May or may not have entities depending on parser — but should not error
97
+ });
98
+ });
99
+ });