@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,285 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { generateIndex } from "../indexer";
3
+ import type { WikiArticle } from "../types";
4
+
5
+ // ─── Helpers ────────────────────────────────────────────────────────────
6
+
7
+ function makeArticle(
8
+ overrides: Partial<WikiArticle> & {
9
+ path: string;
10
+ type: WikiArticle["type"];
11
+ title: string;
12
+ },
13
+ ): WikiArticle {
14
+ return {
15
+ content: "",
16
+ contentHash: "abc123",
17
+ sourceHashes: [],
18
+ backlinks: [],
19
+ forwardLinks: [],
20
+ pageRank: 0,
21
+ lastCompiled: new Date().toISOString(),
22
+ referenceCount: 0,
23
+ ebbinghausScore: 1.0,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ // ─── Tests ──────────────────────────────────────────────────────────────
29
+
30
+ describe("Wiki Indexer", () => {
31
+ describe("generateIndex", () => {
32
+ it("should generate a markdown index with title", () => {
33
+ const index = generateIndex([]);
34
+ expect(index).toContain("# Wiki Index");
35
+ });
36
+
37
+ it("should show article count in the description", () => {
38
+ const articles = [
39
+ makeArticle({
40
+ path: "wiki/modules/auth.md",
41
+ type: "module",
42
+ title: "Auth Module",
43
+ }),
44
+ makeArticle({
45
+ path: "wiki/entities/jwt.md",
46
+ type: "entity",
47
+ title: "JWT",
48
+ }),
49
+ ];
50
+
51
+ const index = generateIndex(articles);
52
+ expect(index).toContain("2 articles");
53
+ });
54
+
55
+ it("should group articles by type", () => {
56
+ const articles = [
57
+ makeArticle({
58
+ path: "wiki/modules/auth.md",
59
+ type: "module",
60
+ title: "Auth Module",
61
+ }),
62
+ makeArticle({
63
+ path: "wiki/entities/jwt.md",
64
+ type: "entity",
65
+ title: "JWT",
66
+ }),
67
+ makeArticle({
68
+ path: "wiki/features/login.md",
69
+ type: "feature",
70
+ title: "Login Feature",
71
+ }),
72
+ makeArticle({
73
+ path: "wiki/decisions/use-jwt.md",
74
+ type: "decision",
75
+ title: "Use JWT",
76
+ }),
77
+ ];
78
+
79
+ const index = generateIndex(articles);
80
+ expect(index).toContain("## Modules");
81
+ expect(index).toContain("## Entities");
82
+ expect(index).toContain("## Features");
83
+ expect(index).toContain("## Decisions");
84
+ });
85
+
86
+ it("should include markdown links to articles", () => {
87
+ const articles = [
88
+ makeArticle({
89
+ path: "wiki/modules/auth.md",
90
+ type: "module",
91
+ title: "Auth Module",
92
+ }),
93
+ ];
94
+
95
+ const index = generateIndex(articles);
96
+ expect(index).toContain("[Auth Module](wiki/modules/auth.md)");
97
+ });
98
+
99
+ it("should sort articles by PageRank within each group", () => {
100
+ const articles = [
101
+ makeArticle({
102
+ path: "wiki/entities/low.md",
103
+ type: "entity",
104
+ title: "Low PR",
105
+ pageRank: 0.1,
106
+ }),
107
+ makeArticle({
108
+ path: "wiki/entities/high.md",
109
+ type: "entity",
110
+ title: "High PR",
111
+ pageRank: 0.9,
112
+ }),
113
+ makeArticle({
114
+ path: "wiki/entities/mid.md",
115
+ type: "entity",
116
+ title: "Mid PR",
117
+ pageRank: 0.5,
118
+ }),
119
+ ];
120
+
121
+ const index = generateIndex(articles);
122
+ const highIdx = index.indexOf("High PR");
123
+ const midIdx = index.indexOf("Mid PR");
124
+ const lowIdx = index.indexOf("Low PR");
125
+
126
+ expect(highIdx).toBeLessThan(midIdx);
127
+ expect(midIdx).toBeLessThan(lowIdx);
128
+ });
129
+
130
+ it("should include freshness indicators", () => {
131
+ const now = new Date().toISOString();
132
+ const articles = [
133
+ makeArticle({
134
+ path: "wiki/modules/fresh.md",
135
+ type: "module",
136
+ title: "Fresh Module",
137
+ lastCompiled: now,
138
+ }),
139
+ ];
140
+
141
+ const index = generateIndex(articles);
142
+ expect(index).toContain("[fresh]");
143
+ });
144
+
145
+ it("should show stale indicator for old articles", () => {
146
+ const oldDate = new Date(
147
+ Date.now() - 60 * 24 * 60 * 60 * 1000,
148
+ ).toISOString();
149
+ const articles = [
150
+ makeArticle({
151
+ path: "wiki/modules/old.md",
152
+ type: "module",
153
+ title: "Old Module",
154
+ lastCompiled: oldDate,
155
+ }),
156
+ ];
157
+
158
+ const index = generateIndex(articles);
159
+ expect(index).toContain("[stale]");
160
+ });
161
+
162
+ it("should show aging indicator for articles 8-30 days old", () => {
163
+ const agingDate = new Date(
164
+ Date.now() - 15 * 24 * 60 * 60 * 1000,
165
+ ).toISOString();
166
+ const articles = [
167
+ makeArticle({
168
+ path: "wiki/modules/aging.md",
169
+ type: "module",
170
+ title: "Aging Module",
171
+ lastCompiled: agingDate,
172
+ }),
173
+ ];
174
+
175
+ const index = generateIndex(articles);
176
+ expect(index).toContain("[aging]");
177
+ });
178
+
179
+ it("should show recent indicator for articles 2-6 days old", () => {
180
+ const recentDate = new Date(
181
+ Date.now() - 3 * 24 * 60 * 60 * 1000,
182
+ ).toISOString();
183
+ const articles = [
184
+ makeArticle({
185
+ path: "wiki/modules/recent.md",
186
+ type: "module",
187
+ title: "Recent Module",
188
+ lastCompiled: recentDate,
189
+ }),
190
+ ];
191
+
192
+ const index = generateIndex(articles);
193
+ expect(index).toContain("[recent]");
194
+ });
195
+
196
+ it("should handle empty lastCompiled as stale", () => {
197
+ const articles = [
198
+ makeArticle({
199
+ path: "wiki/modules/nodate.md",
200
+ type: "module",
201
+ title: "No Date",
202
+ lastCompiled: "",
203
+ }),
204
+ ];
205
+
206
+ const index = generateIndex(articles);
207
+ expect(index).toContain("[stale]");
208
+ });
209
+
210
+ it("should not render sections for types with no articles", () => {
211
+ const articles = [
212
+ makeArticle({
213
+ path: "wiki/modules/auth.md",
214
+ type: "module",
215
+ title: "Auth Module",
216
+ }),
217
+ ];
218
+
219
+ const index = generateIndex(articles);
220
+ expect(index).toContain("## Modules");
221
+ expect(index).not.toContain("## Entities");
222
+ expect(index).not.toContain("## Features");
223
+ expect(index).not.toContain("## Decisions");
224
+ });
225
+
226
+ it("should render type sections in defined order", () => {
227
+ const articles = [
228
+ makeArticle({
229
+ path: "wiki/decisions/d.md",
230
+ type: "decision",
231
+ title: "Decision",
232
+ }),
233
+ makeArticle({
234
+ path: "wiki/features/f.md",
235
+ type: "feature",
236
+ title: "Feature",
237
+ }),
238
+ makeArticle({
239
+ path: "wiki/modules/m.md",
240
+ type: "module",
241
+ title: "Module",
242
+ }),
243
+ makeArticle({
244
+ path: "wiki/entities/e.md",
245
+ type: "entity",
246
+ title: "Entity",
247
+ }),
248
+ ];
249
+
250
+ const index = generateIndex(articles);
251
+ const moduleIdx = index.indexOf("## Modules");
252
+ const entityIdx = index.indexOf("## Entities");
253
+ const featureIdx = index.indexOf("## Features");
254
+ const decisionIdx = index.indexOf("## Decisions");
255
+
256
+ // Order: Architecture > Modules > Entities > Features > Decisions
257
+ expect(moduleIdx).toBeLessThan(entityIdx);
258
+ expect(entityIdx).toBeLessThan(featureIdx);
259
+ expect(featureIdx).toBeLessThan(decisionIdx);
260
+ });
261
+
262
+ it("should report category count correctly", () => {
263
+ const articles = [
264
+ makeArticle({
265
+ path: "wiki/modules/a.md",
266
+ type: "module",
267
+ title: "A",
268
+ }),
269
+ makeArticle({
270
+ path: "wiki/modules/b.md",
271
+ type: "module",
272
+ title: "B",
273
+ }),
274
+ makeArticle({
275
+ path: "wiki/features/c.md",
276
+ type: "feature",
277
+ title: "C",
278
+ }),
279
+ ];
280
+
281
+ const index = generateIndex(articles);
282
+ expect(index).toContain("2 categories");
283
+ });
284
+ });
285
+ });
@@ -0,0 +1,230 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { KnowledgeGraph } from "../graph";
3
+ import { generateLinks } from "../linker";
4
+ import type { EdgeType } from "../types";
5
+
6
+ // ─── Helpers ────────────────────────────────────────────────────────────
7
+
8
+ function makeGraph(
9
+ edges: Array<{ source: string; target: string; type: EdgeType }>,
10
+ ): KnowledgeGraph {
11
+ const nodes = new Map<
12
+ string,
13
+ {
14
+ id: string;
15
+ type: "entity" | "module" | "feature" | "decision" | "workflow";
16
+ label: string;
17
+ pageRank: number;
18
+ }
19
+ >();
20
+ const adjacency = new Map<string, Set<string>>();
21
+
22
+ for (const edge of edges) {
23
+ if (!nodes.has(edge.source)) {
24
+ nodes.set(edge.source, {
25
+ id: edge.source,
26
+ type: "entity",
27
+ label: edge.source,
28
+ pageRank: 0,
29
+ });
30
+ }
31
+ if (!nodes.has(edge.target)) {
32
+ nodes.set(edge.target, {
33
+ id: edge.target,
34
+ type: "entity",
35
+ label: edge.target,
36
+ pageRank: 0,
37
+ });
38
+ }
39
+
40
+ if (!adjacency.has(edge.source)) {
41
+ adjacency.set(edge.source, new Set());
42
+ }
43
+ if (!adjacency.has(edge.target)) {
44
+ adjacency.set(edge.target, new Set());
45
+ }
46
+ adjacency.get(edge.source)?.add(edge.target);
47
+ adjacency.get(edge.target)?.add(edge.source);
48
+ }
49
+
50
+ return {
51
+ nodes,
52
+ edges: edges.map((e) => ({ ...e, weight: 1.0 })),
53
+ adjacency,
54
+ };
55
+ }
56
+
57
+ // ─── Tests ──────────────────────────────────────────────────────────────
58
+
59
+ describe("Wiki Linker", () => {
60
+ describe("generateLinks", () => {
61
+ it("should create forward links from source to target", () => {
62
+ const graph = makeGraph([
63
+ { source: "entity:A", target: "entity:B", type: "calls" },
64
+ ]);
65
+ const articleMap = new Map<string, string>();
66
+ articleMap.set("entity:A", "wiki/entities/A.md");
67
+ articleMap.set("entity:B", "wiki/entities/B.md");
68
+
69
+ const result = generateLinks(graph, articleMap);
70
+
71
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
72
+ expect(aLinks.length).toBe(1);
73
+ expect(aLinks[0]?.target).toBe("wiki/entities/B.md");
74
+ expect(aLinks[0]?.type).toBe("calls");
75
+ });
76
+
77
+ it("should create backlinks from target to source", () => {
78
+ const graph = makeGraph([
79
+ { source: "entity:A", target: "entity:B", type: "calls" },
80
+ ]);
81
+ const articleMap = new Map<string, string>();
82
+ articleMap.set("entity:A", "wiki/entities/A.md");
83
+ articleMap.set("entity:B", "wiki/entities/B.md");
84
+
85
+ const result = generateLinks(graph, articleMap);
86
+
87
+ const bBacklinks = result.backlinks.get("wiki/entities/B.md") ?? [];
88
+ expect(bBacklinks.length).toBe(1);
89
+ expect(bBacklinks[0]?.target).toBe("wiki/entities/A.md");
90
+ });
91
+
92
+ it("should skip edges where source or target has no article", () => {
93
+ const graph = makeGraph([
94
+ { source: "entity:A", target: "entity:B", type: "calls" },
95
+ ]);
96
+ const articleMap = new Map<string, string>();
97
+ articleMap.set("entity:A", "wiki/entities/A.md");
98
+ // entity:B has no article mapping
99
+
100
+ const result = generateLinks(graph, articleMap);
101
+
102
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
103
+ expect(aLinks.length).toBe(0);
104
+ });
105
+
106
+ it("should skip self-referential links (same article)", () => {
107
+ const graph = makeGraph([
108
+ {
109
+ source: "entity:A",
110
+ target: "entity:A",
111
+ type: "specified_by",
112
+ },
113
+ ]);
114
+ const articleMap = new Map<string, string>();
115
+ articleMap.set("entity:A", "wiki/entities/A.md");
116
+
117
+ const result = generateLinks(graph, articleMap);
118
+
119
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
120
+ expect(aLinks.length).toBe(0);
121
+ });
122
+
123
+ it("should deduplicate links with the same target and type", () => {
124
+ const graph = makeGraph([
125
+ { source: "entity:A", target: "entity:B", type: "calls" },
126
+ { source: "entity:A", target: "entity:B", type: "calls" },
127
+ ]);
128
+ const articleMap = new Map<string, string>();
129
+ articleMap.set("entity:A", "wiki/entities/A.md");
130
+ articleMap.set("entity:B", "wiki/entities/B.md");
131
+
132
+ const result = generateLinks(graph, articleMap);
133
+
134
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
135
+ expect(aLinks.length).toBe(1);
136
+ });
137
+
138
+ it("should keep links of different types to the same target", () => {
139
+ const graph = makeGraph([
140
+ { source: "entity:A", target: "entity:B", type: "calls" },
141
+ { source: "entity:A", target: "entity:B", type: "imports" },
142
+ ]);
143
+ const articleMap = new Map<string, string>();
144
+ articleMap.set("entity:A", "wiki/entities/A.md");
145
+ articleMap.set("entity:B", "wiki/entities/B.md");
146
+
147
+ const result = generateLinks(graph, articleMap);
148
+
149
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
150
+ expect(aLinks.length).toBe(2);
151
+ const types = aLinks.map((l) => l.type).sort();
152
+ expect(types).toEqual(["calls", "imports"]);
153
+ });
154
+
155
+ it("should handle all 11 edge types", () => {
156
+ const allEdgeTypes: EdgeType[] = [
157
+ "calls",
158
+ "imports",
159
+ "inherits",
160
+ "references",
161
+ "member_of",
162
+ "modified_by",
163
+ "specified_by",
164
+ "decided_by",
165
+ "motivated_by",
166
+ "constrains",
167
+ "aligns_with",
168
+ ];
169
+
170
+ const edges = allEdgeTypes.map((type, i) => ({
171
+ source: `entity:src-${i}`,
172
+ target: `entity:tgt-${i}`,
173
+ type,
174
+ }));
175
+
176
+ const graph = makeGraph(edges);
177
+ const articleMap = new Map<string, string>();
178
+ for (let i = 0; i < allEdgeTypes.length; i++) {
179
+ articleMap.set(`entity:src-${i}`, `wiki/entities/src-${i}.md`);
180
+ articleMap.set(`entity:tgt-${i}`, `wiki/entities/tgt-${i}.md`);
181
+ }
182
+
183
+ const result = generateLinks(graph, articleMap);
184
+
185
+ // Each edge type should produce at least one forward link
186
+ for (let i = 0; i < allEdgeTypes.length; i++) {
187
+ const links =
188
+ result.forwardLinks.get(`wiki/entities/src-${i}.md`) ?? [];
189
+ expect(links.length).toBeGreaterThanOrEqual(1);
190
+ expect(links[0]?.type).toBe(allEdgeTypes[i]);
191
+ }
192
+ });
193
+
194
+ it("should return empty maps for a graph with no edges", () => {
195
+ const graph: KnowledgeGraph = {
196
+ nodes: new Map(),
197
+ edges: [],
198
+ adjacency: new Map(),
199
+ };
200
+ const articleMap = new Map<string, string>();
201
+
202
+ const result = generateLinks(graph, articleMap);
203
+
204
+ expect(result.forwardLinks.size).toBe(0);
205
+ expect(result.backlinks.size).toBe(0);
206
+ });
207
+
208
+ it("should handle multiple forward links from one article", () => {
209
+ const graph = makeGraph([
210
+ { source: "entity:A", target: "entity:B", type: "calls" },
211
+ { source: "entity:A", target: "entity:C", type: "imports" },
212
+ {
213
+ source: "entity:A",
214
+ target: "entity:D",
215
+ type: "references",
216
+ },
217
+ ]);
218
+ const articleMap = new Map<string, string>();
219
+ articleMap.set("entity:A", "wiki/entities/A.md");
220
+ articleMap.set("entity:B", "wiki/entities/B.md");
221
+ articleMap.set("entity:C", "wiki/entities/C.md");
222
+ articleMap.set("entity:D", "wiki/entities/D.md");
223
+
224
+ const result = generateLinks(graph, articleMap);
225
+
226
+ const aLinks = result.forwardLinks.get("wiki/entities/A.md") ?? [];
227
+ expect(aLinks.length).toBe(3);
228
+ });
229
+ });
230
+ });