@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,344 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CodeEntity } from "../extractors/code";
3
+ import type { KnowledgeGraph } from "../graph";
4
+ import { buildKnowledgeGraph, computePageRank, mapToArticles } from "../graph";
5
+ import type {
6
+ ExtractedDecision,
7
+ ExtractedFeature,
8
+ ExtractedWorkflowTrace,
9
+ } from "../types";
10
+
11
+ // ─── Test Fixtures ──────────────────────────────────────────────────────
12
+
13
+ function makeEntities(): CodeEntity[] {
14
+ return [
15
+ {
16
+ name: "runPipeline",
17
+ kind: "function",
18
+ file: "src/verify/pipeline.ts",
19
+ line: 10,
20
+ exported: true,
21
+ },
22
+ {
23
+ name: "syntaxGuard",
24
+ kind: "function",
25
+ file: "src/verify/syntax.ts",
26
+ line: 5,
27
+ exported: true,
28
+ },
29
+ {
30
+ name: "CacheManager",
31
+ kind: "class",
32
+ file: "src/cache/manager.ts",
33
+ line: 1,
34
+ exported: true,
35
+ },
36
+ {
37
+ name: "hashContent",
38
+ kind: "function",
39
+ file: "src/cache/hash.ts",
40
+ line: 3,
41
+ exported: true,
42
+ },
43
+ ];
44
+ }
45
+
46
+ function makeFeatures(): ExtractedFeature[] {
47
+ return [
48
+ {
49
+ id: "001-auth",
50
+ title: "Authentication",
51
+ scope: "Auth module",
52
+ specQualityScore: 0.8,
53
+ specAssertions: ["JWT tokens expire after 1 hour"],
54
+ tasks: [{ id: "T001", description: "Implement JWT", completed: true }],
55
+ entitiesModified: ["runPipeline"],
56
+ decisionsCreated: ["0001-jwt"],
57
+ branch: "feat/auth",
58
+ prNumber: 1,
59
+ merged: true,
60
+ },
61
+ ];
62
+ }
63
+
64
+ function makeDecisions(): ExtractedDecision[] {
65
+ return [
66
+ {
67
+ id: "0001-jwt",
68
+ title: "Use JWT for Auth",
69
+ status: "accepted",
70
+ context: "Need stateless auth",
71
+ decision: "Use JWT tokens",
72
+ rationale: "Scalable and stateless",
73
+ alternativesRejected: ["Sessions", "OAuth only"],
74
+ entityMentions: ["src/verify/pipeline.ts"],
75
+ constitutionAlignment: ["security-first"],
76
+ },
77
+ ];
78
+ }
79
+
80
+ function makeTraces(): ExtractedWorkflowTrace[] {
81
+ return [
82
+ {
83
+ featureId: "001-auth",
84
+ steps: [
85
+ {
86
+ command: "brainstorm",
87
+ timestamp: "2026-04-07T10:00:00.000Z",
88
+ summary: "Explored auth approaches",
89
+ },
90
+ ],
91
+ wikiRefsRead: [],
92
+ wikiRefsWritten: [],
93
+ rlSignals: [],
94
+ },
95
+ ];
96
+ }
97
+
98
+ // ─── Tests ──────────────────────────────────────────────────────────────
99
+
100
+ describe("Knowledge Graph", () => {
101
+ describe("buildKnowledgeGraph", () => {
102
+ it("should create nodes for code entities", () => {
103
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
104
+ expect(graph.nodes.has("entity:runPipeline")).toBe(true);
105
+ expect(graph.nodes.has("entity:syntaxGuard")).toBe(true);
106
+ expect(graph.nodes.has("entity:CacheManager")).toBe(true);
107
+ expect(graph.nodes.has("entity:hashContent")).toBe(true);
108
+ });
109
+
110
+ it("should create module nodes from entity file paths", () => {
111
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
112
+ expect(graph.nodes.has("module:verify")).toBe(true);
113
+ expect(graph.nodes.has("module:cache")).toBe(true);
114
+ });
115
+
116
+ it("should create member_of edges between entities and modules", () => {
117
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
118
+ const memberOfEdges = graph.edges.filter((e) => e.type === "member_of");
119
+ expect(memberOfEdges.length).toBeGreaterThan(0);
120
+
121
+ const pipelineToVerify = memberOfEdges.find(
122
+ (e) =>
123
+ e.source === "entity:runPipeline" && e.target === "module:verify",
124
+ );
125
+ expect(pipelineToVerify).toBeDefined();
126
+ });
127
+
128
+ it("should create feature nodes", () => {
129
+ const graph = buildKnowledgeGraph(makeEntities(), makeFeatures(), [], []);
130
+ expect(graph.nodes.has("feature:001-auth")).toBe(true);
131
+ expect(graph.nodes.get("feature:001-auth")?.label).toBe("Authentication");
132
+ });
133
+
134
+ it("should create decision nodes", () => {
135
+ const graph = buildKnowledgeGraph(
136
+ makeEntities(),
137
+ [],
138
+ makeDecisions(),
139
+ [],
140
+ );
141
+ expect(graph.nodes.has("decision:0001-jwt")).toBe(true);
142
+ });
143
+
144
+ it("should create workflow nodes", () => {
145
+ const graph = buildKnowledgeGraph(
146
+ makeEntities(),
147
+ makeFeatures(),
148
+ [],
149
+ makeTraces(),
150
+ );
151
+ expect(graph.nodes.has("workflow:001-auth")).toBe(true);
152
+ });
153
+
154
+ it("should create edges for all 11 edge types", () => {
155
+ const graph = buildKnowledgeGraph(
156
+ makeEntities(),
157
+ makeFeatures(),
158
+ makeDecisions(),
159
+ makeTraces(),
160
+ );
161
+
162
+ const edgeTypes = new Set(graph.edges.map((e) => e.type));
163
+
164
+ // Code edges
165
+ expect(edgeTypes.has("member_of")).toBe(true);
166
+ expect(edgeTypes.has("references")).toBe(true);
167
+
168
+ // Lifecycle edges that should be present
169
+ expect(edgeTypes.has("modified_by")).toBe(true);
170
+ expect(edgeTypes.has("specified_by")).toBe(true);
171
+ expect(edgeTypes.has("decided_by")).toBe(true);
172
+ expect(edgeTypes.has("constrains")).toBe(true);
173
+ expect(edgeTypes.has("aligns_with")).toBe(true);
174
+ });
175
+
176
+ it("should build adjacency map for all nodes", () => {
177
+ const graph = buildKnowledgeGraph(
178
+ makeEntities(),
179
+ makeFeatures(),
180
+ makeDecisions(),
181
+ makeTraces(),
182
+ );
183
+
184
+ // Every node should appear in the adjacency map
185
+ for (const nodeId of graph.nodes.keys()) {
186
+ expect(graph.adjacency.has(nodeId)).toBe(true);
187
+ }
188
+ });
189
+
190
+ it("should handle empty inputs", () => {
191
+ const graph = buildKnowledgeGraph([], [], [], []);
192
+ expect(graph.nodes.size).toBe(0);
193
+ expect(graph.edges).toHaveLength(0);
194
+ expect(graph.adjacency.size).toBe(0);
195
+ });
196
+ });
197
+
198
+ describe("computePageRank", () => {
199
+ it("should compute scores that sum to approximately 1", () => {
200
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
201
+ const scores = computePageRank(graph);
202
+
203
+ let sum = 0;
204
+ for (const score of scores.values()) {
205
+ sum += score;
206
+ }
207
+
208
+ expect(sum).toBeCloseTo(1.0, 1);
209
+ });
210
+
211
+ it("should assign higher rank to more connected nodes", () => {
212
+ const graph = buildKnowledgeGraph(
213
+ makeEntities(),
214
+ makeFeatures(),
215
+ makeDecisions(),
216
+ makeTraces(),
217
+ );
218
+ const scores = computePageRank(graph);
219
+
220
+ // runPipeline is connected to many things (feature, decision, module)
221
+ // so it should have relatively high rank
222
+ const runPipelineScore = scores.get("entity:runPipeline") ?? 0;
223
+ expect(runPipelineScore).toBeGreaterThan(0);
224
+ });
225
+
226
+ it("should converge after iterations", () => {
227
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
228
+ const scores10 = computePageRank(graph, 10);
229
+ const scores50 = computePageRank(graph, 50);
230
+
231
+ // Scores should be similar after convergence
232
+ for (const [id, score10] of scores10) {
233
+ const score50 = scores50.get(id) ?? 0;
234
+ expect(Math.abs(score10 - score50)).toBeLessThan(0.01);
235
+ }
236
+ });
237
+
238
+ it("should return empty map for empty graph", () => {
239
+ const graph: KnowledgeGraph = {
240
+ nodes: new Map(),
241
+ edges: [],
242
+ adjacency: new Map(),
243
+ };
244
+ const scores = computePageRank(graph);
245
+ expect(scores.size).toBe(0);
246
+ });
247
+
248
+ it("should update graph node pageRank values", () => {
249
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
250
+ computePageRank(graph);
251
+
252
+ for (const node of graph.nodes.values()) {
253
+ expect(node.pageRank).toBeGreaterThan(0);
254
+ }
255
+ });
256
+ });
257
+
258
+ describe("mapToArticles", () => {
259
+ it("should map top 20% entities to wiki/entities/", () => {
260
+ const graph = buildKnowledgeGraph(
261
+ makeEntities(),
262
+ makeFeatures(),
263
+ makeDecisions(),
264
+ makeTraces(),
265
+ );
266
+ computePageRank(graph);
267
+
268
+ const communities = new Map<number, string[]>();
269
+ communities.set(0, ["module:verify", "entity:runPipeline"]);
270
+ communities.set(1, ["module:cache", "entity:CacheManager"]);
271
+
272
+ const articleMap = mapToArticles(graph, communities);
273
+
274
+ // At least one entity should map to wiki/entities/
275
+ const entityPaths = [...articleMap.values()].filter((p) =>
276
+ p.startsWith("wiki/entities/"),
277
+ );
278
+ expect(entityPaths.length).toBeGreaterThan(0);
279
+ });
280
+
281
+ it("should map communities to wiki/modules/", () => {
282
+ const graph = buildKnowledgeGraph(makeEntities(), [], [], []);
283
+ computePageRank(graph);
284
+
285
+ const communities = new Map<number, string[]>();
286
+ communities.set(0, ["module:verify"]);
287
+ communities.set(1, ["module:cache"]);
288
+
289
+ const articleMap = mapToArticles(graph, communities);
290
+
291
+ const modulePaths = [...articleMap.values()].filter((p) =>
292
+ p.startsWith("wiki/modules/"),
293
+ );
294
+ expect(modulePaths.length).toBe(2);
295
+ });
296
+
297
+ it("should map features to wiki/features/", () => {
298
+ const graph = buildKnowledgeGraph(makeEntities(), makeFeatures(), [], []);
299
+ computePageRank(graph);
300
+
301
+ const articleMap = mapToArticles(graph, new Map());
302
+ const featurePaths = [...articleMap.values()].filter((p) =>
303
+ p.startsWith("wiki/features/"),
304
+ );
305
+ expect(featurePaths.length).toBe(1);
306
+ });
307
+
308
+ it("should map decisions to wiki/decisions/", () => {
309
+ const graph = buildKnowledgeGraph(
310
+ makeEntities(),
311
+ [],
312
+ makeDecisions(),
313
+ [],
314
+ );
315
+ computePageRank(graph);
316
+
317
+ const articleMap = mapToArticles(graph, new Map());
318
+ const decisionPaths = [...articleMap.values()].filter((p) =>
319
+ p.startsWith("wiki/decisions/"),
320
+ );
321
+ expect(decisionPaths.length).toBe(1);
322
+ });
323
+
324
+ it("should sanitize names in article paths", () => {
325
+ const graph = buildKnowledgeGraph(
326
+ makeEntities(),
327
+ makeFeatures(),
328
+ makeDecisions(),
329
+ makeTraces(),
330
+ );
331
+ computePageRank(graph);
332
+
333
+ const articleMap = mapToArticles(
334
+ graph,
335
+ new Map([[0, ["module:verify"]]]),
336
+ );
337
+
338
+ for (const path of articleMap.values()) {
339
+ // Paths should not contain spaces or special chars
340
+ expect(path).toMatch(/^[a-zA-Z0-9/_.-]+$/);
341
+ }
342
+ });
343
+ });
344
+ });
@@ -0,0 +1,119 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createEmptyState, saveState } from "../state";
5
+
6
+ // ── Import under test ──────────────────────────────────────────────────────
7
+
8
+ const { onPostCommit } = await import("../hooks");
9
+
10
+ // ── Test Helpers ────────────────────────────────────────────────────────────
11
+
12
+ let tmpDir: string;
13
+
14
+ function createTmpDir(): string {
15
+ const dir = join(
16
+ import.meta.dir,
17
+ `tmp-wiki-hooks-${Date.now()}-${Math.random().toString(36).slice(2)}`,
18
+ );
19
+ mkdirSync(dir, { recursive: true });
20
+ return dir;
21
+ }
22
+
23
+ /**
24
+ * Set up a minimal wiki directory with state file for testing.
25
+ */
26
+ function setupWikiDir(root: string): string {
27
+ const mainaDir = join(root, ".maina");
28
+ const wikiDir = join(mainaDir, "wiki");
29
+ mkdirSync(join(wikiDir, "modules"), { recursive: true });
30
+ mkdirSync(join(wikiDir, "entities"), { recursive: true });
31
+ mkdirSync(join(wikiDir, "features"), { recursive: true });
32
+ mkdirSync(join(wikiDir, "decisions"), { recursive: true });
33
+ mkdirSync(join(wikiDir, "architecture"), { recursive: true });
34
+ mkdirSync(join(wikiDir, "raw"), { recursive: true });
35
+
36
+ // Create state file (marks wiki as initialized)
37
+ const state = createEmptyState();
38
+ state.lastFullCompile = new Date().toISOString();
39
+ saveState(wikiDir, state);
40
+
41
+ return mainaDir;
42
+ }
43
+
44
+ /**
45
+ * Create a minimal source file so the compiler has something to work with.
46
+ */
47
+ function createSampleSource(root: string): void {
48
+ const srcDir = join(root, "packages", "core", "src");
49
+ mkdirSync(srcDir, { recursive: true });
50
+ writeFileSync(
51
+ join(srcDir, "index.ts"),
52
+ "export function hello(): string { return 'hello'; }\n",
53
+ );
54
+ }
55
+
56
+ beforeEach(() => {
57
+ tmpDir = createTmpDir();
58
+ });
59
+
60
+ afterEach(() => {
61
+ try {
62
+ rmSync(tmpDir, { recursive: true, force: true });
63
+ } catch {
64
+ // ignore
65
+ }
66
+ });
67
+
68
+ // ── Tests ───────────────────────────────────────────────────────────────────
69
+
70
+ describe("Wiki Post-Commit Hook", () => {
71
+ it("triggers incremental compile when wiki is initialized", async () => {
72
+ createSampleSource(tmpDir);
73
+ const mainaDir = setupWikiDir(tmpDir);
74
+
75
+ // Should complete without throwing
76
+ await onPostCommit(mainaDir, tmpDir);
77
+
78
+ // Verify it ran by checking state was updated
79
+ const { loadState } = await import("../state");
80
+ const wikiDir = join(mainaDir, "wiki");
81
+ const state = loadState(wikiDir);
82
+ expect(state).not.toBeNull();
83
+ // The compilation should have updated lastIncrementalCompile
84
+ expect(state?.lastIncrementalCompile).toBeTruthy();
85
+ });
86
+
87
+ it("skips when wiki directory does not exist", async () => {
88
+ const mainaDir = join(tmpDir, ".maina");
89
+ mkdirSync(mainaDir, { recursive: true });
90
+ // No wiki directory — should skip silently
91
+
92
+ await onPostCommit(mainaDir, tmpDir);
93
+
94
+ // Should complete without error — nothing to assert except no throw
95
+ expect(true).toBe(true);
96
+ });
97
+
98
+ it("skips when wiki not initialized (no .state.json)", async () => {
99
+ const mainaDir = join(tmpDir, ".maina");
100
+ const wikiDir = join(mainaDir, "wiki");
101
+ mkdirSync(wikiDir, { recursive: true });
102
+ // Wiki directory exists but no .state.json
103
+
104
+ await onPostCommit(mainaDir, tmpDir);
105
+
106
+ // Should complete without error
107
+ expect(true).toBe(true);
108
+ });
109
+
110
+ it("swallows errors gracefully", async () => {
111
+ // Pass a completely invalid directory to trigger internal errors
112
+ const bogusDir = join(tmpDir, "nonexistent", ".maina");
113
+
114
+ // Should NOT throw — errors are swallowed
115
+ await onPostCommit(bogusDir, tmpDir);
116
+
117
+ expect(true).toBe(true);
118
+ });
119
+ });