@mainahq/core 1.0.2 → 1.1.0
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 +1 -1
- package/src/ai/__tests__/availability.test.ts +131 -0
- package/src/ai/__tests__/delegation.test.ts +55 -1
- package/src/ai/availability.ts +23 -0
- package/src/ai/delegation.ts +5 -3
- package/src/context/__tests__/budget.test.ts +29 -6
- package/src/context/__tests__/engine.test.ts +1 -0
- package/src/context/__tests__/selector.test.ts +23 -3
- package/src/context/__tests__/wiki.test.ts +349 -0
- package/src/context/budget.ts +12 -8
- package/src/context/engine.ts +37 -0
- package/src/context/selector.ts +30 -4
- package/src/context/wiki.ts +296 -0
- package/src/db/index.ts +12 -0
- package/src/feedback/__tests__/capture.test.ts +166 -0
- package/src/feedback/__tests__/signals.test.ts +144 -0
- package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
- package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
- package/src/feedback/capture.ts +102 -0
- package/src/feedback/signals.ts +68 -0
- package/src/index.ts +108 -1
- package/src/init/__tests__/init.test.ts +477 -18
- package/src/init/index.ts +419 -13
- package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
- package/src/prompts/defaults/index.ts +3 -1
- package/src/prompts/defaults/wiki-compile.md +20 -0
- package/src/prompts/defaults/wiki-query.md +18 -0
- package/src/stats/__tests__/tool-usage.test.ts +133 -0
- package/src/stats/tracker.ts +92 -0
- package/src/verify/__tests__/builtin.test.ts +270 -0
- package/src/verify/__tests__/pipeline.test.ts +11 -8
- package/src/verify/builtin.ts +350 -0
- package/src/verify/pipeline.ts +32 -2
- package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
- package/src/verify/tools/wiki-lint-runner.ts +38 -0
- package/src/verify/tools/wiki-lint.ts +898 -0
- package/src/wiki/__tests__/compiler.test.ts +389 -0
- package/src/wiki/__tests__/extractors/code.test.ts +99 -0
- package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
- package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
- package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
- package/src/wiki/__tests__/graph.test.ts +344 -0
- package/src/wiki/__tests__/hooks.test.ts +119 -0
- package/src/wiki/__tests__/indexer.test.ts +285 -0
- package/src/wiki/__tests__/linker.test.ts +230 -0
- package/src/wiki/__tests__/louvain.test.ts +229 -0
- package/src/wiki/__tests__/query.test.ts +316 -0
- package/src/wiki/__tests__/schema.test.ts +114 -0
- package/src/wiki/__tests__/signals.test.ts +474 -0
- package/src/wiki/__tests__/state.test.ts +168 -0
- package/src/wiki/__tests__/tracking.test.ts +118 -0
- package/src/wiki/__tests__/types.test.ts +387 -0
- package/src/wiki/compiler.ts +1075 -0
- package/src/wiki/extractors/code.ts +90 -0
- package/src/wiki/extractors/decision.ts +217 -0
- package/src/wiki/extractors/feature.ts +206 -0
- package/src/wiki/extractors/workflow.ts +112 -0
- package/src/wiki/graph.ts +445 -0
- package/src/wiki/hooks.ts +49 -0
- package/src/wiki/indexer.ts +105 -0
- package/src/wiki/linker.ts +117 -0
- package/src/wiki/louvain.ts +190 -0
- package/src/wiki/prompts/compile-architecture.md +59 -0
- package/src/wiki/prompts/compile-decision.md +66 -0
- package/src/wiki/prompts/compile-entity.md +56 -0
- package/src/wiki/prompts/compile-feature.md +60 -0
- package/src/wiki/prompts/compile-module.md +42 -0
- package/src/wiki/prompts/wiki-query.md +25 -0
- package/src/wiki/query.ts +338 -0
- package/src/wiki/schema.ts +111 -0
- package/src/wiki/signals.ts +368 -0
- package/src/wiki/state.ts +89 -0
- package/src/wiki/tracking.ts +30 -0
- package/src/wiki/types.ts +169 -0
- package/src/workflow/context.ts +26 -0
|
@@ -0,0 +1,349 @@
|
|
|
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 { calculateTokens } from "../budget";
|
|
12
|
+
import { assembleWikiText, loadWikiContext } from "../wiki";
|
|
13
|
+
|
|
14
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function makeTempDir(): string {
|
|
17
|
+
const dir = join(
|
|
18
|
+
tmpdir(),
|
|
19
|
+
`maina-wiki-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
20
|
+
);
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeWikiFile(
|
|
26
|
+
wikiDir: string,
|
|
27
|
+
relPath: string,
|
|
28
|
+
content: string,
|
|
29
|
+
): void {
|
|
30
|
+
const fullPath = join(wikiDir, relPath);
|
|
31
|
+
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
writeFileSync(fullPath, content, "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("loadWikiContext", () => {
|
|
39
|
+
let wikiDir: string;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
wikiDir = makeTempDir();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
try {
|
|
47
|
+
rmSync(wikiDir, { recursive: true, force: true });
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore cleanup errors
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns null when wiki directory does not exist", () => {
|
|
54
|
+
const result = loadWikiContext({
|
|
55
|
+
wikiDir: join(tmpdir(), "nonexistent-wiki-dir-xyz"),
|
|
56
|
+
});
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns null when wiki directory is empty", () => {
|
|
61
|
+
// wikiDir exists but has no files
|
|
62
|
+
const result = loadWikiContext({ wikiDir });
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("loads index.md when wiki exists", () => {
|
|
67
|
+
writeWikiFile(wikiDir, "index.md", "# Wiki Index\nOverview of articles.");
|
|
68
|
+
|
|
69
|
+
const result = loadWikiContext({ wikiDir });
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
expect(result?.text).toContain("Wiki Index");
|
|
72
|
+
expect(result?.text).toContain("Overview of articles");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("loads decision articles for review command", () => {
|
|
76
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
77
|
+
writeWikiFile(
|
|
78
|
+
wikiDir,
|
|
79
|
+
"decisions/adr-001.md",
|
|
80
|
+
"# ADR-001: Use TypeScript\nWe chose TypeScript for type safety.",
|
|
81
|
+
);
|
|
82
|
+
writeWikiFile(
|
|
83
|
+
wikiDir,
|
|
84
|
+
"features/feature-auth.md",
|
|
85
|
+
"# Auth Feature\nAuthentication implementation details.",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = loadWikiContext({ wikiDir, command: "review" });
|
|
89
|
+
expect(result).not.toBeNull();
|
|
90
|
+
expect(result?.text).toContain("ADR-001");
|
|
91
|
+
// review loads decisions and modules, not features
|
|
92
|
+
expect(result?.text).not.toContain("Auth Feature");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("loads feature articles for commit command", () => {
|
|
96
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
97
|
+
writeWikiFile(
|
|
98
|
+
wikiDir,
|
|
99
|
+
"features/feature-wiki.md",
|
|
100
|
+
"# Wiki Feature\nWiki context layer details.",
|
|
101
|
+
);
|
|
102
|
+
writeWikiFile(
|
|
103
|
+
wikiDir,
|
|
104
|
+
"decisions/adr-001.md",
|
|
105
|
+
"# ADR-001: TypeScript\nDecision content.",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const result = loadWikiContext({ wikiDir, command: "commit" });
|
|
109
|
+
expect(result).not.toBeNull();
|
|
110
|
+
expect(result?.text).toContain("Wiki Feature");
|
|
111
|
+
// commit loads features and architecture, not decisions
|
|
112
|
+
expect(result?.text).not.toContain("ADR-001");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("loads all article types for explain command (exploration mode)", () => {
|
|
116
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
117
|
+
writeWikiFile(
|
|
118
|
+
wikiDir,
|
|
119
|
+
"decisions/adr-001.md",
|
|
120
|
+
"# ADR-001\nDecision content.",
|
|
121
|
+
);
|
|
122
|
+
writeWikiFile(
|
|
123
|
+
wikiDir,
|
|
124
|
+
"features/feature-auth.md",
|
|
125
|
+
"# Auth Feature\nAuth details.",
|
|
126
|
+
);
|
|
127
|
+
writeWikiFile(
|
|
128
|
+
wikiDir,
|
|
129
|
+
"modules/context-engine.md",
|
|
130
|
+
"# Context Engine\nEngine docs.",
|
|
131
|
+
);
|
|
132
|
+
writeWikiFile(
|
|
133
|
+
wikiDir,
|
|
134
|
+
"architecture/overview.md",
|
|
135
|
+
"# Architecture Overview\nSystem design.",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = loadWikiContext({ wikiDir, command: "explain" });
|
|
139
|
+
expect(result).not.toBeNull();
|
|
140
|
+
expect(result?.text).toContain("ADR-001");
|
|
141
|
+
expect(result?.text).toContain("Auth Feature");
|
|
142
|
+
expect(result?.text).toContain("Context Engine");
|
|
143
|
+
expect(result?.text).toContain("Architecture Overview");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("loads decision articles for design command (conflict detection)", () => {
|
|
147
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
148
|
+
writeWikiFile(
|
|
149
|
+
wikiDir,
|
|
150
|
+
"decisions/adr-001.md",
|
|
151
|
+
"# ADR-001\nDecision content.",
|
|
152
|
+
);
|
|
153
|
+
writeWikiFile(
|
|
154
|
+
wikiDir,
|
|
155
|
+
"features/feature-auth.md",
|
|
156
|
+
"# Auth Feature\nShould not be loaded.",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = loadWikiContext({ wikiDir, command: "design" });
|
|
160
|
+
expect(result).not.toBeNull();
|
|
161
|
+
expect(result?.text).toContain("ADR-001");
|
|
162
|
+
expect(result?.text).not.toContain("Auth Feature");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("respects token budget (returns content within reasonable bounds)", () => {
|
|
166
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
167
|
+
// Create several articles
|
|
168
|
+
for (let i = 0; i < 10; i++) {
|
|
169
|
+
writeWikiFile(
|
|
170
|
+
wikiDir,
|
|
171
|
+
`decisions/adr-${String(i).padStart(3, "0")}.md`,
|
|
172
|
+
`# ADR-${i}\n${"Content line. ".repeat(100)}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = loadWikiContext({ wikiDir, command: "review" });
|
|
177
|
+
expect(result).not.toBeNull();
|
|
178
|
+
expect(result?.tokens).toBeGreaterThan(0);
|
|
179
|
+
// Tokens should match calculateTokens of the text
|
|
180
|
+
expect(result?.tokens).toBe(calculateTokens(result?.text ?? ""));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns valid LayerContent with correct name and priority", () => {
|
|
184
|
+
writeWikiFile(wikiDir, "index.md", "# Wiki Index");
|
|
185
|
+
|
|
186
|
+
const result = loadWikiContext({ wikiDir });
|
|
187
|
+
expect(result).not.toBeNull();
|
|
188
|
+
expect(result?.name).toBe("wiki");
|
|
189
|
+
expect(result?.priority).toBe(4);
|
|
190
|
+
expect(typeof result?.text).toBe("string");
|
|
191
|
+
expect(typeof result?.tokens).toBe("number");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("handles empty wiki directory gracefully", () => {
|
|
195
|
+
// Create a subdirectory but no files
|
|
196
|
+
mkdirSync(join(wikiDir, "decisions"), { recursive: true });
|
|
197
|
+
|
|
198
|
+
const result = loadWikiContext({ wikiDir });
|
|
199
|
+
// Directory has an entry (subdirectory) but no actual content
|
|
200
|
+
expect(result).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("boosts articles relevant to working files", () => {
|
|
204
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
205
|
+
writeWikiFile(
|
|
206
|
+
wikiDir,
|
|
207
|
+
"modules/context-engine.md",
|
|
208
|
+
"# Context Engine\nRelevant to working files.",
|
|
209
|
+
);
|
|
210
|
+
writeWikiFile(
|
|
211
|
+
wikiDir,
|
|
212
|
+
"modules/git-ops.md",
|
|
213
|
+
"# Git Operations\nNot relevant to working files.",
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const result = loadWikiContext({
|
|
217
|
+
wikiDir,
|
|
218
|
+
workingFiles: ["packages/core/src/context/engine.ts"],
|
|
219
|
+
command: "review",
|
|
220
|
+
});
|
|
221
|
+
expect(result).not.toBeNull();
|
|
222
|
+
// Both should be loaded since both are modules and command is review
|
|
223
|
+
expect(result?.text).toContain("Context Engine");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("returns null when wiki dir has only empty subdirectories", () => {
|
|
227
|
+
mkdirSync(join(wikiDir, "decisions"), { recursive: true });
|
|
228
|
+
mkdirSync(join(wikiDir, "modules"), { recursive: true });
|
|
229
|
+
|
|
230
|
+
const result = loadWikiContext({ wikiDir });
|
|
231
|
+
expect(result).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("includes index content in output even without other articles", () => {
|
|
235
|
+
writeWikiFile(wikiDir, "index.md", "# Project Wiki\nThis is the index.");
|
|
236
|
+
|
|
237
|
+
const result = loadWikiContext({ wikiDir });
|
|
238
|
+
expect(result).not.toBeNull();
|
|
239
|
+
expect(result?.text).toContain("## Wiki Knowledge (Layer 5)");
|
|
240
|
+
expect(result?.text).toContain("### Index");
|
|
241
|
+
expect(result?.text).toContain("Project Wiki");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("formats articles with wiki/ path prefix", () => {
|
|
245
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
246
|
+
writeWikiFile(
|
|
247
|
+
wikiDir,
|
|
248
|
+
"decisions/adr-001.md",
|
|
249
|
+
"# ADR-001: Use Bun\nBun is faster.",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const result = loadWikiContext({ wikiDir, command: "review" });
|
|
253
|
+
expect(result).not.toBeNull();
|
|
254
|
+
expect(result?.text).toContain("wiki/decisions/adr-001.md");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("loads all articles when no command is specified (default)", () => {
|
|
258
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
259
|
+
writeWikiFile(
|
|
260
|
+
wikiDir,
|
|
261
|
+
"decisions/adr-001.md",
|
|
262
|
+
"# Decision\nDecision content.",
|
|
263
|
+
);
|
|
264
|
+
writeWikiFile(
|
|
265
|
+
wikiDir,
|
|
266
|
+
"features/feature.md",
|
|
267
|
+
"# Feature\nFeature content.",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const result = loadWikiContext({ wikiDir });
|
|
271
|
+
expect(result).not.toBeNull();
|
|
272
|
+
expect(result?.text).toContain("Decision");
|
|
273
|
+
expect(result?.text).toContain("Feature");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("records loaded articles to signals file for RL tracking", () => {
|
|
277
|
+
writeWikiFile(wikiDir, "index.md", "# Index");
|
|
278
|
+
writeWikiFile(
|
|
279
|
+
wikiDir,
|
|
280
|
+
"decisions/adr-001.md",
|
|
281
|
+
"# ADR-001\nDecision content.",
|
|
282
|
+
);
|
|
283
|
+
writeWikiFile(
|
|
284
|
+
wikiDir,
|
|
285
|
+
"modules/context-engine.md",
|
|
286
|
+
"# Context Engine\nEngine docs.",
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const result = loadWikiContext({ wikiDir, command: "review" });
|
|
290
|
+
expect(result).not.toBeNull();
|
|
291
|
+
|
|
292
|
+
// Check that the signals file was created
|
|
293
|
+
const signalsPath = join(wikiDir, ".signals.json");
|
|
294
|
+
expect(existsSync(signalsPath)).toBe(true);
|
|
295
|
+
|
|
296
|
+
const raw = readFileSync(signalsPath, "utf-8");
|
|
297
|
+
const parsed = JSON.parse(raw);
|
|
298
|
+
expect(parsed.loadSignals).toBeDefined();
|
|
299
|
+
expect(parsed.loadSignals.length).toBeGreaterThan(0);
|
|
300
|
+
expect(parsed.loadSignals[0].command).toBe("review");
|
|
301
|
+
expect(Array.isArray(parsed.loadSignals[0].articles)).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("assembleWikiText", () => {
|
|
306
|
+
it("formats with correct headers", () => {
|
|
307
|
+
const text = assembleWikiText("# Index content", [
|
|
308
|
+
{
|
|
309
|
+
path: "decisions/adr-001.md",
|
|
310
|
+
title: "ADR-001",
|
|
311
|
+
content: "Decision content.",
|
|
312
|
+
category: "decision",
|
|
313
|
+
score: 0.9,
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
expect(text).toContain("## Wiki Knowledge (Layer 5)");
|
|
318
|
+
expect(text).toContain("### Index");
|
|
319
|
+
expect(text).toContain("# Index content");
|
|
320
|
+
expect(text).toContain("### Relevant Articles");
|
|
321
|
+
expect(text).toContain("#### ADR-001 (wiki/decisions/adr-001.md)");
|
|
322
|
+
expect(text).toContain("Decision content.");
|
|
323
|
+
expect(text).toContain("---");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("handles empty index", () => {
|
|
327
|
+
const text = assembleWikiText("", [
|
|
328
|
+
{
|
|
329
|
+
path: "decisions/adr-001.md",
|
|
330
|
+
title: "ADR-001",
|
|
331
|
+
content: "Content.",
|
|
332
|
+
category: "decision",
|
|
333
|
+
score: 0.9,
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
expect(text).toContain("## Wiki Knowledge (Layer 5)");
|
|
338
|
+
expect(text).not.toContain("### Index");
|
|
339
|
+
expect(text).toContain("### Relevant Articles");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("handles empty articles", () => {
|
|
343
|
+
const text = assembleWikiText("# Index", []);
|
|
344
|
+
|
|
345
|
+
expect(text).toContain("## Wiki Knowledge (Layer 5)");
|
|
346
|
+
expect(text).toContain("### Index");
|
|
347
|
+
expect(text).not.toContain("### Relevant Articles");
|
|
348
|
+
});
|
|
349
|
+
});
|
package/src/context/budget.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface BudgetAllocation {
|
|
|
5
5
|
episodic: number;
|
|
6
6
|
semantic: number;
|
|
7
7
|
retrieval: number;
|
|
8
|
+
wiki: number;
|
|
8
9
|
total: number;
|
|
9
10
|
headroom: number; // reserved for AI reasoning
|
|
10
11
|
}
|
|
@@ -45,10 +46,11 @@ export function getBudgetRatio(mode: BudgetMode): number {
|
|
|
45
46
|
* Calculates per-layer token allocations for a given mode and model context window.
|
|
46
47
|
*
|
|
47
48
|
* Layer proportions within the usable budget:
|
|
48
|
-
* working: ~
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* retrieval: ~
|
|
49
|
+
* working: ~12%
|
|
50
|
+
* episodic: ~12%
|
|
51
|
+
* semantic: ~16%
|
|
52
|
+
* retrieval: ~8% (remainder after the above four)
|
|
53
|
+
* wiki: ~12%
|
|
52
54
|
*/
|
|
53
55
|
export function assembleBudget(
|
|
54
56
|
mode: BudgetMode,
|
|
@@ -58,17 +60,19 @@ export function assembleBudget(
|
|
|
58
60
|
const budget = Math.floor(modelContextWindow * ratio);
|
|
59
61
|
const headroom = modelContextWindow - budget;
|
|
60
62
|
|
|
61
|
-
const working = Math.floor(budget * 0.
|
|
62
|
-
const
|
|
63
|
-
const
|
|
63
|
+
const working = Math.floor(budget * 0.12);
|
|
64
|
+
const episodic = Math.floor(budget * 0.12);
|
|
65
|
+
const semantic = Math.floor(budget * 0.16);
|
|
66
|
+
const wiki = Math.floor(budget * 0.12);
|
|
64
67
|
// retrieval gets the exact remainder so all layer tokens sum to budget
|
|
65
|
-
const retrieval = budget - working - semantic -
|
|
68
|
+
const retrieval = budget - working - episodic - semantic - wiki;
|
|
66
69
|
|
|
67
70
|
return {
|
|
68
71
|
working,
|
|
69
72
|
semantic,
|
|
70
73
|
episodic,
|
|
71
74
|
retrieval,
|
|
75
|
+
wiki,
|
|
72
76
|
headroom,
|
|
73
77
|
total: modelContextWindow,
|
|
74
78
|
};
|
package/src/context/engine.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./retrieval";
|
|
22
22
|
import type { MainaCommand } from "./selector";
|
|
23
23
|
import { getBudgetMode, getContextNeeds, needsLayer } from "./selector";
|
|
24
|
+
import { loadWikiContext } from "./wiki";
|
|
24
25
|
import { assembleWorkingText, loadWorkingContext } from "./working";
|
|
25
26
|
|
|
26
27
|
// ── Public types ──────────────────────────────────────────────────────────────
|
|
@@ -418,6 +419,42 @@ export async function assembleContext(
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
|
|
422
|
+
// Wiki layer — synchronous, wrapped in a promise for parallel execution
|
|
423
|
+
if (needsLayer(needs, "wiki")) {
|
|
424
|
+
const wikiDir = join(mainaDir, "wiki");
|
|
425
|
+
// Gather working files from a quick git check
|
|
426
|
+
let workingFiles: string[] | undefined;
|
|
427
|
+
try {
|
|
428
|
+
const [staged, changed] = await Promise.all([
|
|
429
|
+
getStagedFiles(repoRoot),
|
|
430
|
+
getChangedFiles("HEAD~3", repoRoot),
|
|
431
|
+
]);
|
|
432
|
+
workingFiles = [...new Set([...staged, ...changed])];
|
|
433
|
+
} catch {
|
|
434
|
+
workingFiles = undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const wikiResult = loadWikiContext({
|
|
438
|
+
wikiDir,
|
|
439
|
+
workingFiles,
|
|
440
|
+
command,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (wikiResult !== null) {
|
|
444
|
+
layerPromises.push(Promise.resolve(wikiResult));
|
|
445
|
+
} else {
|
|
446
|
+
// Empty placeholder so it appears in reports
|
|
447
|
+
layerPromises.push(
|
|
448
|
+
Promise.resolve({
|
|
449
|
+
name: "wiki",
|
|
450
|
+
text: "",
|
|
451
|
+
tokens: 0,
|
|
452
|
+
priority: 4,
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
421
458
|
// Build all layers in parallel
|
|
422
459
|
const builtLayers = await Promise.all(layerPromises);
|
|
423
460
|
|
package/src/context/selector.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface ContextNeeds {
|
|
|
17
17
|
episodic: boolean | string[];
|
|
18
18
|
semantic: boolean | string[];
|
|
19
19
|
retrieval: boolean;
|
|
20
|
+
wiki: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const CONTEXT_NEEDS: Record<MainaCommand, ContextNeeds> = {
|
|
@@ -25,45 +26,70 @@ const CONTEXT_NEEDS: Record<MainaCommand, ContextNeeds> = {
|
|
|
25
26
|
episodic: false,
|
|
26
27
|
semantic: ["conventions"],
|
|
27
28
|
retrieval: false,
|
|
29
|
+
wiki: true,
|
|
28
30
|
},
|
|
29
31
|
verify: {
|
|
30
32
|
working: true,
|
|
31
33
|
episodic: ["recent-reviews"],
|
|
32
34
|
semantic: ["adrs", "conventions"],
|
|
33
35
|
retrieval: false,
|
|
36
|
+
wiki: true,
|
|
37
|
+
},
|
|
38
|
+
context: {
|
|
39
|
+
working: true,
|
|
40
|
+
episodic: true,
|
|
41
|
+
semantic: true,
|
|
42
|
+
retrieval: true,
|
|
43
|
+
wiki: true,
|
|
34
44
|
},
|
|
35
|
-
context: { working: true, episodic: true, semantic: true, retrieval: true },
|
|
36
45
|
review: {
|
|
37
46
|
working: true,
|
|
38
47
|
episodic: ["past-reviews"],
|
|
39
48
|
semantic: ["adrs"],
|
|
40
49
|
retrieval: false,
|
|
50
|
+
wiki: true,
|
|
41
51
|
},
|
|
42
52
|
plan: {
|
|
43
53
|
working: true,
|
|
44
54
|
semantic: ["adrs", "conventions"],
|
|
45
55
|
episodic: false,
|
|
46
56
|
retrieval: false,
|
|
57
|
+
wiki: true,
|
|
58
|
+
},
|
|
59
|
+
explain: {
|
|
60
|
+
working: true,
|
|
61
|
+
episodic: false,
|
|
62
|
+
semantic: true,
|
|
63
|
+
retrieval: true,
|
|
64
|
+
wiki: true,
|
|
47
65
|
},
|
|
48
|
-
explain: { working: true, episodic: false, semantic: true, retrieval: true },
|
|
49
66
|
design: {
|
|
50
67
|
working: true,
|
|
51
68
|
episodic: false,
|
|
52
69
|
semantic: ["adrs"],
|
|
53
70
|
retrieval: false,
|
|
71
|
+
wiki: true,
|
|
54
72
|
},
|
|
55
73
|
ticket: {
|
|
56
74
|
working: false,
|
|
57
75
|
episodic: false,
|
|
58
76
|
semantic: ["modules"],
|
|
59
77
|
retrieval: false,
|
|
78
|
+
wiki: false,
|
|
79
|
+
},
|
|
80
|
+
analyze: {
|
|
81
|
+
working: true,
|
|
82
|
+
episodic: true,
|
|
83
|
+
semantic: true,
|
|
84
|
+
retrieval: false,
|
|
85
|
+
wiki: true,
|
|
60
86
|
},
|
|
61
|
-
analyze: { working: true, episodic: true, semantic: true, retrieval: false },
|
|
62
87
|
pr: {
|
|
63
88
|
working: true,
|
|
64
89
|
episodic: ["past-reviews"],
|
|
65
90
|
semantic: true,
|
|
66
91
|
retrieval: true,
|
|
92
|
+
wiki: true,
|
|
67
93
|
},
|
|
68
94
|
};
|
|
69
95
|
|
|
@@ -73,7 +99,7 @@ export function getContextNeeds(command: MainaCommand): ContextNeeds {
|
|
|
73
99
|
|
|
74
100
|
export function needsLayer(
|
|
75
101
|
needs: ContextNeeds,
|
|
76
|
-
layer: "working" | "episodic" | "semantic" | "retrieval",
|
|
102
|
+
layer: "working" | "episodic" | "semantic" | "retrieval" | "wiki",
|
|
77
103
|
): boolean {
|
|
78
104
|
const value = needs[layer];
|
|
79
105
|
if (Array.isArray(value)) {
|