@mainahq/core 0.2.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/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- package/src/workflow/context.ts +81 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { SearchResult } from "../retrieval";
|
|
3
|
+
import {
|
|
4
|
+
assembleRetrievalText,
|
|
5
|
+
isToolAvailable,
|
|
6
|
+
parseZoektOutput,
|
|
7
|
+
search,
|
|
8
|
+
} from "../retrieval";
|
|
9
|
+
|
|
10
|
+
describe("isToolAvailable", () => {
|
|
11
|
+
test("returns true for 'git' (always available)", async () => {
|
|
12
|
+
const result = await isToolAvailable("git");
|
|
13
|
+
expect(result).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns false for 'nonexistent-tool-xyz'", async () => {
|
|
17
|
+
const result = await isToolAvailable("nonexistent-tool-xyz");
|
|
18
|
+
expect(result).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("search", () => {
|
|
23
|
+
test("finds known symbol in codebase", async () => {
|
|
24
|
+
const results = await search("getCurrentBranch", {
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
});
|
|
27
|
+
expect(results.length).toBeGreaterThan(0);
|
|
28
|
+
// At least one result should contain the search term
|
|
29
|
+
const hasMatch = results.some((r) =>
|
|
30
|
+
r.content.includes("getCurrentBranch"),
|
|
31
|
+
);
|
|
32
|
+
expect(hasMatch).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("respects maxResults limit", async () => {
|
|
36
|
+
// Search for something that appears many times (e.g. 'export')
|
|
37
|
+
const results = await search("export", {
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
maxResults: 3,
|
|
40
|
+
});
|
|
41
|
+
expect(results.length).toBeLessThanOrEqual(3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns empty array for nonsense query", async () => {
|
|
45
|
+
// Use a pattern that cannot appear in real source code
|
|
46
|
+
const needle = ["ZZZZ", "QQQQ", "NOSUCHSYMBOL", "9999"].join("__");
|
|
47
|
+
const results = await search(needle, {
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
});
|
|
50
|
+
expect(results).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("returns an array (never throws)", async () => {
|
|
54
|
+
// Even with a bad cwd, should not throw
|
|
55
|
+
const results = await search("anything", {
|
|
56
|
+
cwd: "/nonexistent/path/xyz",
|
|
57
|
+
});
|
|
58
|
+
expect(Array.isArray(results)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("parseZoektOutput", () => {
|
|
63
|
+
test("parses zoekt text output into SearchResult[]", () => {
|
|
64
|
+
const output = `src/app.ts:10:export function main() {
|
|
65
|
+
src/app.ts:11: return true;
|
|
66
|
+
src/utils.ts:5:export const helper = () => {};`;
|
|
67
|
+
|
|
68
|
+
const results = parseZoektOutput(output);
|
|
69
|
+
expect(results).toHaveLength(3);
|
|
70
|
+
expect(results[0]?.filePath).toBe("src/app.ts");
|
|
71
|
+
expect(results[0]?.line).toBe(10);
|
|
72
|
+
expect(results[0]?.content).toBe("export function main() {");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("handles empty output", () => {
|
|
76
|
+
expect(parseZoektOutput("")).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("skips malformed lines", () => {
|
|
80
|
+
const output = `src/app.ts:10:valid line
|
|
81
|
+
not a valid line
|
|
82
|
+
another:bad
|
|
83
|
+
src/utils.ts:5:also valid`;
|
|
84
|
+
const results = parseZoektOutput(output);
|
|
85
|
+
expect(results).toHaveLength(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("handles zoekt header lines gracefully", () => {
|
|
89
|
+
const output = `Repository: maina
|
|
90
|
+
src/app.ts:10:export function main() {
|
|
91
|
+
src/app.ts:11: return true;`;
|
|
92
|
+
const results = parseZoektOutput(output);
|
|
93
|
+
// Should skip the Repository: line
|
|
94
|
+
expect(results).toHaveLength(2);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("searchWithZoekt", () => {
|
|
99
|
+
test("should skip when zoekt is not available", async () => {
|
|
100
|
+
// zoekt is almost certainly not installed in test environment
|
|
101
|
+
const { searchWithZoekt } = await import("../retrieval");
|
|
102
|
+
const results = await searchWithZoekt("testquery", { cwd: process.cwd() });
|
|
103
|
+
// Either returns results (if zoekt installed) or empty array (not installed)
|
|
104
|
+
expect(Array.isArray(results)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("assembleRetrievalText", () => {
|
|
109
|
+
test("formats results as 'filePath:line: content'", () => {
|
|
110
|
+
const results: SearchResult[] = [
|
|
111
|
+
{
|
|
112
|
+
filePath: "src/foo.ts",
|
|
113
|
+
line: 10,
|
|
114
|
+
content: "export function foo() {}",
|
|
115
|
+
matchLength: 3,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
filePath: "src/bar.ts",
|
|
119
|
+
line: 42,
|
|
120
|
+
content: "const bar = 1;",
|
|
121
|
+
matchLength: 3,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
const text = assembleRetrievalText(results);
|
|
125
|
+
expect(text).toContain("src/foo.ts:10: export function foo() {}");
|
|
126
|
+
expect(text).toContain("src/bar.ts:42: const bar = 1;");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("returns empty string for empty results", () => {
|
|
130
|
+
const text = assembleRetrievalText([]);
|
|
131
|
+
expect(text).toBe("");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("separates results with newlines", () => {
|
|
135
|
+
const results: SearchResult[] = [
|
|
136
|
+
{ filePath: "a.ts", line: 1, content: "line one", matchLength: 4 },
|
|
137
|
+
{ filePath: "b.ts", line: 2, content: "line two", matchLength: 4 },
|
|
138
|
+
];
|
|
139
|
+
const text = assembleRetrievalText(results);
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
expect(lines.length).toBe(2);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { BudgetMode } from "../budget.ts";
|
|
3
|
+
import { getBudgetMode, getContextNeeds, needsLayer } from "../selector.ts";
|
|
4
|
+
|
|
5
|
+
describe("getContextNeeds", () => {
|
|
6
|
+
it("commit needs only working + conventions", () => {
|
|
7
|
+
const needs = getContextNeeds("commit");
|
|
8
|
+
expect(needs.working).toBe(true);
|
|
9
|
+
expect(needs.episodic).toBe(false);
|
|
10
|
+
expect(needs.semantic).toEqual(["conventions"]);
|
|
11
|
+
expect(needs.retrieval).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("context command needs all 4 layers", () => {
|
|
15
|
+
const needs = getContextNeeds("context");
|
|
16
|
+
expect(needs.working).toBe(true);
|
|
17
|
+
expect(needs.episodic).toBe(true);
|
|
18
|
+
expect(needs.semantic).toBe(true);
|
|
19
|
+
expect(needs.retrieval).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("verify needs working + recent-reviews + adrs + conventions", () => {
|
|
23
|
+
const needs = getContextNeeds("verify");
|
|
24
|
+
expect(needs.working).toBe(true);
|
|
25
|
+
expect(needs.episodic).toEqual(["recent-reviews"]);
|
|
26
|
+
expect(needs.semantic).toEqual(["adrs", "conventions"]);
|
|
27
|
+
expect(needs.retrieval).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("review returns correct context needs", () => {
|
|
31
|
+
const needs = getContextNeeds("review");
|
|
32
|
+
expect(needs.working).toBe(true);
|
|
33
|
+
expect(needs.episodic).toEqual(["past-reviews"]);
|
|
34
|
+
expect(needs.semantic).toEqual(["adrs"]);
|
|
35
|
+
expect(needs.retrieval).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("plan returns correct context needs", () => {
|
|
39
|
+
const needs = getContextNeeds("plan");
|
|
40
|
+
expect(needs.working).toBe(true);
|
|
41
|
+
expect(needs.semantic).toEqual(["adrs", "conventions"]);
|
|
42
|
+
expect(needs.episodic).toBe(false);
|
|
43
|
+
expect(needs.retrieval).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("explain returns correct context needs", () => {
|
|
47
|
+
const needs = getContextNeeds("explain");
|
|
48
|
+
expect(needs.working).toBe(true);
|
|
49
|
+
expect(needs.episodic).toBe(false);
|
|
50
|
+
expect(needs.semantic).toBe(true);
|
|
51
|
+
expect(needs.retrieval).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("design returns correct context needs", () => {
|
|
55
|
+
const needs = getContextNeeds("design");
|
|
56
|
+
expect(needs.working).toBe(true);
|
|
57
|
+
expect(needs.episodic).toBe(false);
|
|
58
|
+
expect(needs.semantic).toEqual(["adrs"]);
|
|
59
|
+
expect(needs.retrieval).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("ticket returns correct context needs", () => {
|
|
63
|
+
const needs = getContextNeeds("ticket");
|
|
64
|
+
expect(needs.working).toBe(false);
|
|
65
|
+
expect(needs.episodic).toBe(false);
|
|
66
|
+
expect(needs.semantic).toEqual(["modules"]);
|
|
67
|
+
expect(needs.retrieval).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("analyze returns correct context needs", () => {
|
|
71
|
+
const needs = getContextNeeds("analyze");
|
|
72
|
+
expect(needs.working).toBe(true);
|
|
73
|
+
expect(needs.episodic).toBe(true);
|
|
74
|
+
expect(needs.semantic).toBe(true);
|
|
75
|
+
expect(needs.retrieval).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("pr returns correct context needs", () => {
|
|
79
|
+
const needs = getContextNeeds("pr");
|
|
80
|
+
expect(needs.working).toBe(true);
|
|
81
|
+
expect(needs.episodic).toEqual(["past-reviews"]);
|
|
82
|
+
expect(needs.semantic).toBe(true);
|
|
83
|
+
expect(needs.retrieval).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("needsLayer", () => {
|
|
88
|
+
it("correctly identifies when working layer is needed", () => {
|
|
89
|
+
const needs = getContextNeeds("commit");
|
|
90
|
+
expect(needsLayer(needs, "working")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("correctly identifies when working layer is not needed", () => {
|
|
94
|
+
const needs = getContextNeeds("ticket");
|
|
95
|
+
expect(needsLayer(needs, "working")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("correctly identifies when episodic layer is needed (true)", () => {
|
|
99
|
+
const needs = getContextNeeds("context");
|
|
100
|
+
expect(needsLayer(needs, "episodic")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("correctly identifies when episodic layer is needed (string[])", () => {
|
|
104
|
+
const needs = getContextNeeds("verify");
|
|
105
|
+
expect(needsLayer(needs, "episodic")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("correctly identifies when episodic layer is not needed", () => {
|
|
109
|
+
const needs = getContextNeeds("commit");
|
|
110
|
+
expect(needsLayer(needs, "episodic")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("correctly identifies when semantic layer is needed (true)", () => {
|
|
114
|
+
const needs = getContextNeeds("context");
|
|
115
|
+
expect(needsLayer(needs, "semantic")).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("correctly identifies when semantic layer is needed (string[])", () => {
|
|
119
|
+
const needs = getContextNeeds("commit");
|
|
120
|
+
expect(needsLayer(needs, "semantic")).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("correctly identifies when retrieval layer is needed", () => {
|
|
124
|
+
const needs = getContextNeeds("context");
|
|
125
|
+
expect(needsLayer(needs, "retrieval")).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("correctly identifies when retrieval layer is not needed", () => {
|
|
129
|
+
const needs = getContextNeeds("commit");
|
|
130
|
+
expect(needsLayer(needs, "retrieval")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("getBudgetMode", () => {
|
|
135
|
+
it("commit uses focused budget mode", () => {
|
|
136
|
+
expect(getBudgetMode("commit")).toBe("focused" satisfies BudgetMode);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("context uses explore budget mode", () => {
|
|
140
|
+
expect(getBudgetMode("context")).toBe("explore" satisfies BudgetMode);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("explain uses explore budget mode", () => {
|
|
144
|
+
expect(getBudgetMode("explain")).toBe("explore" satisfies BudgetMode);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("review uses default budget mode", () => {
|
|
148
|
+
expect(getBudgetMode("review")).toBe("default" satisfies BudgetMode);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("verify uses default budget mode", () => {
|
|
152
|
+
expect(getBudgetMode("verify")).toBe("default" satisfies BudgetMode);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("plan uses default budget mode", () => {
|
|
156
|
+
expect(getBudgetMode("plan")).toBe("default" satisfies BudgetMode);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("design uses default budget mode", () => {
|
|
160
|
+
expect(getBudgetMode("design")).toBe("default" satisfies BudgetMode);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("ticket uses default budget mode", () => {
|
|
164
|
+
expect(getBudgetMode("ticket")).toBe("default" satisfies BudgetMode);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("analyze uses default budget mode", () => {
|
|
168
|
+
expect(getBudgetMode("analyze")).toBe("default" satisfies BudgetMode);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("pr uses default budget mode", () => {
|
|
172
|
+
expect(getBudgetMode("pr")).toBe("default" satisfies BudgetMode);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } 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 type { DependencyGraph } from "../relevance";
|
|
6
|
+
import {
|
|
7
|
+
assembleSemanticText,
|
|
8
|
+
getTopEntities,
|
|
9
|
+
loadConstitution,
|
|
10
|
+
loadCustomContext,
|
|
11
|
+
type SemanticContext,
|
|
12
|
+
} from "../semantic";
|
|
13
|
+
|
|
14
|
+
const TEST_DIR = join(tmpdir(), `maina-semantic-test-${Date.now()}`);
|
|
15
|
+
const MAINA_DIR = join(TEST_DIR, ".maina");
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
19
|
+
mkdirSync(MAINA_DIR, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("loadConstitution", () => {
|
|
27
|
+
test("returns null when constitution.md does not exist", async () => {
|
|
28
|
+
const result = await loadConstitution(MAINA_DIR);
|
|
29
|
+
expect(result).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("reads content when constitution.md exists", async () => {
|
|
33
|
+
const constitutionPath = join(MAINA_DIR, "constitution.md");
|
|
34
|
+
const content = "# Project Constitution\n\nUse TypeScript everywhere.\n";
|
|
35
|
+
writeFileSync(constitutionPath, content);
|
|
36
|
+
|
|
37
|
+
const result = await loadConstitution(MAINA_DIR);
|
|
38
|
+
expect(result).toBe(content);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("loadCustomContext", () => {
|
|
43
|
+
test("returns empty array when custom directory does not exist", async () => {
|
|
44
|
+
const freshMainaDir = join(TEST_DIR, ".maina-fresh");
|
|
45
|
+
mkdirSync(freshMainaDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const result = await loadCustomContext(freshMainaDir);
|
|
48
|
+
expect(result).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("reads files from the custom directory", async () => {
|
|
52
|
+
const customDir = join(MAINA_DIR, "context", "semantic", "custom");
|
|
53
|
+
mkdirSync(customDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(customDir, "conventions.md"),
|
|
57
|
+
"# Conventions\n\nUse tabs.\n",
|
|
58
|
+
);
|
|
59
|
+
writeFileSync(
|
|
60
|
+
join(customDir, "adrs.md"),
|
|
61
|
+
"# ADRs\n\nADR-001: Use SQLite.\n",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const result = await loadCustomContext(MAINA_DIR);
|
|
65
|
+
expect(result.length).toBe(2);
|
|
66
|
+
|
|
67
|
+
// Should contain the file contents (order may vary)
|
|
68
|
+
const combined = result.join("\n");
|
|
69
|
+
expect(combined).toContain("Conventions");
|
|
70
|
+
expect(combined).toContain("ADRs");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns empty array for empty custom directory", async () => {
|
|
74
|
+
const emptyMainaDir = join(TEST_DIR, ".maina-empty");
|
|
75
|
+
const emptyCustomDir = join(emptyMainaDir, "context", "semantic", "custom");
|
|
76
|
+
mkdirSync(emptyCustomDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const result = await loadCustomContext(emptyMainaDir);
|
|
79
|
+
expect(result).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Helper to build a minimal SemanticContext for testing
|
|
84
|
+
function makeContext(
|
|
85
|
+
overrides: Partial<SemanticContext> = {},
|
|
86
|
+
): SemanticContext {
|
|
87
|
+
const graph: DependencyGraph = {
|
|
88
|
+
nodes: new Set(["fileA.ts", "fileB.ts"]),
|
|
89
|
+
edges: new Map(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
entities: [
|
|
94
|
+
{
|
|
95
|
+
filePath: "fileA.ts",
|
|
96
|
+
name: "doSomething",
|
|
97
|
+
kind: "function",
|
|
98
|
+
relevance: 0.8,
|
|
99
|
+
},
|
|
100
|
+
{ filePath: "fileA.ts", name: "MyClass", kind: "class", relevance: 0.6 },
|
|
101
|
+
{
|
|
102
|
+
filePath: "fileB.ts",
|
|
103
|
+
name: "helper",
|
|
104
|
+
kind: "function",
|
|
105
|
+
relevance: 0.3,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
filePath: "fileB.ts",
|
|
109
|
+
name: "IConfig",
|
|
110
|
+
kind: "interface",
|
|
111
|
+
relevance: 0.2,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
filePath: "fileA.ts",
|
|
115
|
+
name: "lowRelevance",
|
|
116
|
+
kind: "variable",
|
|
117
|
+
relevance: 0.1,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
graph,
|
|
121
|
+
scores: new Map([
|
|
122
|
+
["fileA.ts", 0.7],
|
|
123
|
+
["fileB.ts", 0.3],
|
|
124
|
+
]),
|
|
125
|
+
constitution: null,
|
|
126
|
+
customContext: [],
|
|
127
|
+
...overrides,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("getTopEntities", () => {
|
|
132
|
+
test("returns top N entities by relevance score", () => {
|
|
133
|
+
const context = makeContext();
|
|
134
|
+
const top3 = getTopEntities(context, 3);
|
|
135
|
+
|
|
136
|
+
expect(top3).toHaveLength(3);
|
|
137
|
+
expect(top3[0]?.name).toBe("doSomething");
|
|
138
|
+
expect(top3[1]?.name).toBe("MyClass");
|
|
139
|
+
expect(top3[2]?.name).toBe("helper");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("returns top 20 by default", () => {
|
|
143
|
+
// Create context with fewer than 20 entities
|
|
144
|
+
const context = makeContext();
|
|
145
|
+
const result = getTopEntities(context);
|
|
146
|
+
|
|
147
|
+
// Since we only have 5 entities, all should be returned
|
|
148
|
+
expect(result.length).toBeLessThanOrEqual(20);
|
|
149
|
+
expect(result.length).toBe(5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("returns limited results when n is smaller than total", () => {
|
|
153
|
+
const context = makeContext();
|
|
154
|
+
const top1 = getTopEntities(context, 1);
|
|
155
|
+
|
|
156
|
+
expect(top1).toHaveLength(1);
|
|
157
|
+
expect(top1[0]?.name).toBe("doSomething");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns all entities when n is larger than total", () => {
|
|
161
|
+
const context = makeContext();
|
|
162
|
+
const result = getTopEntities(context, 100);
|
|
163
|
+
|
|
164
|
+
expect(result).toHaveLength(5);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("entities are sorted by relevance descending", () => {
|
|
168
|
+
const context = makeContext();
|
|
169
|
+
const result = getTopEntities(context);
|
|
170
|
+
|
|
171
|
+
for (let i = 1; i < result.length; i++) {
|
|
172
|
+
const prev = result[i - 1]?.relevance ?? 0;
|
|
173
|
+
const curr = result[i]?.relevance ?? 0;
|
|
174
|
+
expect(prev).toBeGreaterThanOrEqual(curr);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("assembleSemanticText", () => {
|
|
180
|
+
test("includes constitution when present", () => {
|
|
181
|
+
const context = makeContext({
|
|
182
|
+
constitution: "# My Constitution\n\nAlways write tests first.\n",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const text = assembleSemanticText(context);
|
|
186
|
+
expect(text).toContain("Constitution");
|
|
187
|
+
expect(text).toContain("Always write tests first");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("does not include constitution section when null", () => {
|
|
191
|
+
const context = makeContext({ constitution: null });
|
|
192
|
+
|
|
193
|
+
const text = assembleSemanticText(context);
|
|
194
|
+
// Should not have an empty constitution block
|
|
195
|
+
expect(text).not.toContain("Always write tests first");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("includes custom context when present", () => {
|
|
199
|
+
const context = makeContext({
|
|
200
|
+
customContext: [
|
|
201
|
+
"# Conventions\n\nUse tabs.",
|
|
202
|
+
"# ADRs\n\nADR-001: SQLite.",
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const text = assembleSemanticText(context);
|
|
207
|
+
expect(text).toContain("Conventions");
|
|
208
|
+
expect(text).toContain("ADRs");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("includes top entities section", () => {
|
|
212
|
+
const context = makeContext();
|
|
213
|
+
const text = assembleSemanticText(context);
|
|
214
|
+
|
|
215
|
+
expect(text).toContain("doSomething");
|
|
216
|
+
expect(text).toContain("MyClass");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("with filter 'conventions' only includes relevant parts", () => {
|
|
220
|
+
const context = makeContext({
|
|
221
|
+
constitution: "# My Constitution\n\nAlways write tests first.\n",
|
|
222
|
+
customContext: [
|
|
223
|
+
"# Conventions\n\nUse tabs.",
|
|
224
|
+
"# ADRs\n\nADR-001: SQLite.",
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const text = assembleSemanticText(context, ["conventions"]);
|
|
229
|
+
expect(text).toContain("Conventions");
|
|
230
|
+
// When filtered, might not include all sections — at minimum includes conventions
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("with filter 'adrs' only includes ADR content", () => {
|
|
234
|
+
const context = makeContext({
|
|
235
|
+
constitution: "# My Constitution\n\nAlways write tests first.\n",
|
|
236
|
+
customContext: [
|
|
237
|
+
"# Conventions\n\nUse tabs.",
|
|
238
|
+
"# ADRs\n\nADR-001: SQLite.",
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const text = assembleSemanticText(context, ["adrs"]);
|
|
243
|
+
expect(text).toContain("ADR");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("returns non-empty string for minimal context", () => {
|
|
247
|
+
const context = makeContext();
|
|
248
|
+
const text = assembleSemanticText(context);
|
|
249
|
+
|
|
250
|
+
expect(text.length).toBeGreaterThan(0);
|
|
251
|
+
});
|
|
252
|
+
});
|