@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,152 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { listStories, loadStory } from "../story-loader";
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = join(
|
|
11
|
+
import.meta.dir,
|
|
12
|
+
`tmp-stories-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
13
|
+
);
|
|
14
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function createStory(
|
|
26
|
+
name: string,
|
|
27
|
+
config: Record<string, unknown>,
|
|
28
|
+
spec = "# Test Spec\n",
|
|
29
|
+
testContent = 'test("stub", () => {});',
|
|
30
|
+
) {
|
|
31
|
+
const storyDir = join(tmpDir, name);
|
|
32
|
+
mkdirSync(join(storyDir, "tests"), { recursive: true });
|
|
33
|
+
writeFileSync(join(storyDir, "story.json"), JSON.stringify(config));
|
|
34
|
+
writeFileSync(join(storyDir, "spec.md"), spec);
|
|
35
|
+
writeFileSync(join(storyDir, "tests", "test.ts"), testContent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("listStories", () => {
|
|
39
|
+
test("returns empty array when stories directory does not exist", () => {
|
|
40
|
+
const result = listStories(join(tmpDir, "nonexistent"));
|
|
41
|
+
expect(result.ok).toBe(true);
|
|
42
|
+
if (result.ok) {
|
|
43
|
+
expect(result.value).toEqual([]);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("lists stories with valid story.json", () => {
|
|
48
|
+
createStory("mitt", {
|
|
49
|
+
name: "mitt",
|
|
50
|
+
description: "Event emitter",
|
|
51
|
+
tier: 1,
|
|
52
|
+
source: "https://github.com/developit/mitt",
|
|
53
|
+
testFiles: ["tests/test.ts"],
|
|
54
|
+
metrics: { expectedTests: 18, originalLOC: 80, complexity: "easy" },
|
|
55
|
+
});
|
|
56
|
+
createStory("ms", {
|
|
57
|
+
name: "ms",
|
|
58
|
+
description: "Time converter",
|
|
59
|
+
tier: 2,
|
|
60
|
+
source: "https://github.com/vercel/ms",
|
|
61
|
+
testFiles: ["tests/test.ts"],
|
|
62
|
+
metrics: { expectedTests: 50, originalLOC: 200, complexity: "medium" },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = listStories(tmpDir);
|
|
66
|
+
expect(result.ok).toBe(true);
|
|
67
|
+
if (result.ok) {
|
|
68
|
+
expect(result.value).toHaveLength(2);
|
|
69
|
+
expect(result.value.map((s) => s.name).sort()).toEqual(["mitt", "ms"]);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("skips directories without story.json", () => {
|
|
74
|
+
createStory("valid", {
|
|
75
|
+
name: "valid",
|
|
76
|
+
description: "V",
|
|
77
|
+
tier: 1,
|
|
78
|
+
source: "s",
|
|
79
|
+
testFiles: ["tests/test.ts"],
|
|
80
|
+
metrics: { expectedTests: 1, originalLOC: 10, complexity: "easy" },
|
|
81
|
+
});
|
|
82
|
+
mkdirSync(join(tmpDir, "no-config"), { recursive: true });
|
|
83
|
+
|
|
84
|
+
const result = listStories(tmpDir);
|
|
85
|
+
expect(result.ok).toBe(true);
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
expect(result.value).toHaveLength(1);
|
|
88
|
+
expect(result.value[0]?.name).toBe("valid");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("loadStory", () => {
|
|
94
|
+
test("loads a valid story with config, spec, and tests", () => {
|
|
95
|
+
createStory(
|
|
96
|
+
"mitt",
|
|
97
|
+
{
|
|
98
|
+
name: "mitt",
|
|
99
|
+
description: "Event emitter",
|
|
100
|
+
tier: 1,
|
|
101
|
+
source: "https://github.com/developit/mitt",
|
|
102
|
+
testFiles: ["tests/test.ts"],
|
|
103
|
+
metrics: { expectedTests: 18, originalLOC: 80, complexity: "easy" },
|
|
104
|
+
},
|
|
105
|
+
"# Mitt Spec\nRequirements here.",
|
|
106
|
+
'import { test } from "bun:test";\ntest("foo", () => {});',
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = loadStory(tmpDir, "mitt");
|
|
110
|
+
expect(result.ok).toBe(true);
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
expect(result.value.config.name).toBe("mitt");
|
|
113
|
+
expect(result.value.specContent).toContain("# Mitt Spec");
|
|
114
|
+
expect(result.value.testFiles).toHaveLength(1);
|
|
115
|
+
expect(result.value.testFiles[0]?.content).toContain('test("foo"');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("returns error for nonexistent story", () => {
|
|
120
|
+
const result = loadStory(tmpDir, "nonexistent");
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("returns error for missing spec.md", () => {
|
|
125
|
+
const storyDir = join(tmpDir, "bad");
|
|
126
|
+
mkdirSync(storyDir, { recursive: true });
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(storyDir, "story.json"),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
name: "bad",
|
|
131
|
+
description: "B",
|
|
132
|
+
tier: 1,
|
|
133
|
+
source: "s",
|
|
134
|
+
testFiles: [],
|
|
135
|
+
metrics: { expectedTests: 0, originalLOC: 0, complexity: "easy" },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const result = loadStory(tmpDir, "bad");
|
|
140
|
+
expect(result.ok).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("returns error for invalid story.json", () => {
|
|
144
|
+
const storyDir = join(tmpDir, "invalid");
|
|
145
|
+
mkdirSync(storyDir, { recursive: true });
|
|
146
|
+
writeFileSync(join(storyDir, "story.json"), "not json");
|
|
147
|
+
writeFileSync(join(storyDir, "spec.md"), "# Spec\n");
|
|
148
|
+
|
|
149
|
+
const result = loadStory(tmpDir, "invalid");
|
|
150
|
+
expect(result.ok).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BenchmarkMetrics,
|
|
3
|
+
BenchmarkReport,
|
|
4
|
+
StepMetrics,
|
|
5
|
+
StoryConfig,
|
|
6
|
+
Tier3Results,
|
|
7
|
+
Tier3Totals,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a comparison report from two pipeline runs.
|
|
12
|
+
*/
|
|
13
|
+
export function buildReport(
|
|
14
|
+
story: StoryConfig,
|
|
15
|
+
maina: BenchmarkMetrics | null,
|
|
16
|
+
speckit: BenchmarkMetrics | null,
|
|
17
|
+
): BenchmarkReport {
|
|
18
|
+
let winner: BenchmarkReport["winner"] = "incomplete";
|
|
19
|
+
|
|
20
|
+
if (maina && speckit) {
|
|
21
|
+
if (maina.testsPassed > speckit.testsPassed) {
|
|
22
|
+
winner = "maina";
|
|
23
|
+
} else if (speckit.testsPassed > maina.testsPassed) {
|
|
24
|
+
winner = "speckit";
|
|
25
|
+
} else {
|
|
26
|
+
winner = "tie";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
story,
|
|
32
|
+
maina,
|
|
33
|
+
speckit,
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
winner,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metricValue(
|
|
40
|
+
metrics: BenchmarkMetrics | null,
|
|
41
|
+
key: keyof BenchmarkMetrics,
|
|
42
|
+
): string {
|
|
43
|
+
if (!metrics) return "—";
|
|
44
|
+
const val = metrics[key];
|
|
45
|
+
if (typeof val === "number") return String(val);
|
|
46
|
+
return String(val);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format a comparison report as a readable terminal table.
|
|
51
|
+
*/
|
|
52
|
+
export function formatComparison(report: BenchmarkReport): string {
|
|
53
|
+
const rows: Array<[string, string, string]> = [
|
|
54
|
+
["Metric", "maina", "speckit"],
|
|
55
|
+
["─".repeat(24), "─".repeat(12), "─".repeat(12)],
|
|
56
|
+
[
|
|
57
|
+
"Tests Passed",
|
|
58
|
+
metricValue(report.maina, "testsPassed"),
|
|
59
|
+
metricValue(report.speckit, "testsPassed"),
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"Tests Failed",
|
|
63
|
+
metricValue(report.maina, "testsFailed"),
|
|
64
|
+
metricValue(report.speckit, "testsFailed"),
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"Tests Total",
|
|
68
|
+
metricValue(report.maina, "testsTotal"),
|
|
69
|
+
metricValue(report.speckit, "testsTotal"),
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"Wall Clock (ms)",
|
|
73
|
+
metricValue(report.maina, "wallClockMs"),
|
|
74
|
+
metricValue(report.speckit, "wallClockMs"),
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"Tokens In",
|
|
78
|
+
metricValue(report.maina, "tokensInput"),
|
|
79
|
+
metricValue(report.speckit, "tokensInput"),
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
"Tokens Out",
|
|
83
|
+
metricValue(report.maina, "tokensOutput"),
|
|
84
|
+
metricValue(report.speckit, "tokensOutput"),
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"Verify Findings",
|
|
88
|
+
metricValue(report.maina, "verifyFindings"),
|
|
89
|
+
metricValue(report.speckit, "verifyFindings"),
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"Spec Quality",
|
|
93
|
+
metricValue(report.maina, "specQualityScore"),
|
|
94
|
+
metricValue(report.speckit, "specQualityScore"),
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"Impl LOC",
|
|
98
|
+
metricValue(report.maina, "implLOC"),
|
|
99
|
+
metricValue(report.speckit, "implLOC"),
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"Attempts to Pass",
|
|
103
|
+
metricValue(report.maina, "attemptsToPass"),
|
|
104
|
+
metricValue(report.speckit, "attemptsToPass"),
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
"Bugs Introduced",
|
|
108
|
+
metricValue(report.maina, "bugsIntroduced"),
|
|
109
|
+
metricValue(report.speckit, "bugsIntroduced"),
|
|
110
|
+
],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const lines = [
|
|
114
|
+
`\n## Benchmark: ${report.story.name} (tier ${report.story.tier})\n`,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const [label, m, s] of rows) {
|
|
118
|
+
lines.push(` ${label.padEnd(24)} ${m.padStart(12)} ${s.padStart(12)}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(` Winner: ${report.winner}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute totals from a record of per-step metrics plus bug/test metadata.
|
|
130
|
+
*/
|
|
131
|
+
function computeTotals(
|
|
132
|
+
steps: Record<string, StepMetrics>,
|
|
133
|
+
meta: {
|
|
134
|
+
bugsIntroduced: number;
|
|
135
|
+
bugsCaught: number;
|
|
136
|
+
testsPassed: number;
|
|
137
|
+
testsTotal: number;
|
|
138
|
+
},
|
|
139
|
+
): Tier3Totals {
|
|
140
|
+
let durationMs = 0;
|
|
141
|
+
let tokensInput = 0;
|
|
142
|
+
let tokensOutput = 0;
|
|
143
|
+
for (const step of Object.values(steps)) {
|
|
144
|
+
durationMs += step.durationMs;
|
|
145
|
+
tokensInput += step.tokensInput;
|
|
146
|
+
tokensOutput += step.tokensOutput;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
durationMs,
|
|
150
|
+
tokensInput,
|
|
151
|
+
tokensOutput,
|
|
152
|
+
bugsIntroduced: meta.bugsIntroduced,
|
|
153
|
+
bugsCaught: meta.bugsCaught,
|
|
154
|
+
testsPassed: meta.testsPassed,
|
|
155
|
+
testsTotal: meta.testsTotal,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Determine winner for tier 3 based on:
|
|
161
|
+
* 1. Test pass rate (higher wins)
|
|
162
|
+
* 2. Bugs caught (higher wins)
|
|
163
|
+
* 3. Duration (lower wins)
|
|
164
|
+
*/
|
|
165
|
+
function determineTier3Winner(
|
|
166
|
+
maina: Tier3Totals,
|
|
167
|
+
speckit: Tier3Totals,
|
|
168
|
+
): Tier3Results["winner"] {
|
|
169
|
+
const mainaPassRate =
|
|
170
|
+
maina.testsTotal > 0 ? maina.testsPassed / maina.testsTotal : 0;
|
|
171
|
+
const speckitPassRate =
|
|
172
|
+
speckit.testsTotal > 0 ? speckit.testsPassed / speckit.testsTotal : 0;
|
|
173
|
+
|
|
174
|
+
if (mainaPassRate !== speckitPassRate) {
|
|
175
|
+
return mainaPassRate > speckitPassRate ? "maina" : "speckit";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (maina.bugsCaught !== speckit.bugsCaught) {
|
|
179
|
+
return maina.bugsCaught > speckit.bugsCaught ? "maina" : "speckit";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (maina.durationMs !== speckit.durationMs) {
|
|
183
|
+
return maina.durationMs < speckit.durationMs ? "maina" : "speckit";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return "tie";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build a tier 3 report from per-step metrics for both pipelines.
|
|
191
|
+
*/
|
|
192
|
+
export function buildTier3Report(
|
|
193
|
+
story: StoryConfig,
|
|
194
|
+
mainaSteps: Record<string, StepMetrics>,
|
|
195
|
+
speckitSteps: Record<string, StepMetrics>,
|
|
196
|
+
learnings: string[],
|
|
197
|
+
meta?: {
|
|
198
|
+
maina: {
|
|
199
|
+
bugsIntroduced: number;
|
|
200
|
+
bugsCaught: number;
|
|
201
|
+
testsPassed: number;
|
|
202
|
+
testsTotal: number;
|
|
203
|
+
};
|
|
204
|
+
speckit: {
|
|
205
|
+
bugsIntroduced: number;
|
|
206
|
+
bugsCaught: number;
|
|
207
|
+
testsPassed: number;
|
|
208
|
+
testsTotal: number;
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
): Tier3Results {
|
|
212
|
+
const mainaMeta = meta?.maina ?? {
|
|
213
|
+
bugsIntroduced: 0,
|
|
214
|
+
bugsCaught: 0,
|
|
215
|
+
testsPassed: 0,
|
|
216
|
+
testsTotal: 0,
|
|
217
|
+
};
|
|
218
|
+
const speckitMeta = meta?.speckit ?? {
|
|
219
|
+
bugsIntroduced: 0,
|
|
220
|
+
bugsCaught: 0,
|
|
221
|
+
testsPassed: 0,
|
|
222
|
+
testsTotal: 0,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const mainaTotals = computeTotals(mainaSteps, mainaMeta);
|
|
226
|
+
const speckitTotals = computeTotals(speckitSteps, speckitMeta);
|
|
227
|
+
|
|
228
|
+
const hasMainaSteps = Object.keys(mainaSteps).length > 0;
|
|
229
|
+
const hasSpeckitSteps = Object.keys(speckitSteps).length > 0;
|
|
230
|
+
|
|
231
|
+
const winner =
|
|
232
|
+
hasMainaSteps && hasSpeckitSteps
|
|
233
|
+
? determineTier3Winner(mainaTotals, speckitTotals)
|
|
234
|
+
: "incomplete";
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
story,
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
maina: { steps: mainaSteps, totals: mainaTotals },
|
|
240
|
+
speckit: { steps: speckitSteps, totals: speckitTotals },
|
|
241
|
+
winner,
|
|
242
|
+
learnings,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Format a tier 3 comparison report as a readable terminal table
|
|
248
|
+
* with per-step breakdown.
|
|
249
|
+
*/
|
|
250
|
+
export function formatTier3Comparison(results: Tier3Results): string {
|
|
251
|
+
const allStepKeys = new Set<string>([
|
|
252
|
+
...Object.keys(results.maina.steps),
|
|
253
|
+
...Object.keys(results.speckit.steps),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const header: [string, string, string, string, string] = [
|
|
257
|
+
"Step",
|
|
258
|
+
"Maina (ms)",
|
|
259
|
+
"Maina (tokens)",
|
|
260
|
+
"SpecKit (ms)",
|
|
261
|
+
"SpecKit (tokens)",
|
|
262
|
+
];
|
|
263
|
+
const separator: [string, string, string, string, string] = [
|
|
264
|
+
"─".repeat(24),
|
|
265
|
+
"─".repeat(14),
|
|
266
|
+
"─".repeat(16),
|
|
267
|
+
"─".repeat(14),
|
|
268
|
+
"─".repeat(16),
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const rows: Array<[string, string, string, string, string]> = [
|
|
272
|
+
header,
|
|
273
|
+
separator,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
for (const key of allStepKeys) {
|
|
277
|
+
const ms = results.maina.steps[key];
|
|
278
|
+
const ss = results.speckit.steps[key];
|
|
279
|
+
rows.push([
|
|
280
|
+
ms?.name ?? ss?.name ?? key,
|
|
281
|
+
ms ? String(ms.durationMs) : "—",
|
|
282
|
+
ms ? String(ms.tokensInput + ms.tokensOutput) : "—",
|
|
283
|
+
ss ? String(ss.durationMs) : "—",
|
|
284
|
+
ss ? String(ss.tokensInput + ss.tokensOutput) : "—",
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Totals row
|
|
289
|
+
const mt = results.maina.totals;
|
|
290
|
+
const st = results.speckit.totals;
|
|
291
|
+
rows.push(separator);
|
|
292
|
+
rows.push([
|
|
293
|
+
"TOTAL",
|
|
294
|
+
String(mt.durationMs),
|
|
295
|
+
String(mt.tokensInput + mt.tokensOutput),
|
|
296
|
+
String(st.durationMs),
|
|
297
|
+
String(st.tokensInput + st.tokensOutput),
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const lines = [`\n## Tier 3 Benchmark: ${results.story.name}\n`];
|
|
301
|
+
|
|
302
|
+
for (const [step, mMs, mTok, sMs, sTok] of rows) {
|
|
303
|
+
lines.push(
|
|
304
|
+
` ${step.padEnd(24)} ${mMs.padStart(14)} ${mTok.padStart(16)} ${sMs.padStart(14)} ${sTok.padStart(16)}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Findings/bugs summary
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push(" Findings / Bugs:");
|
|
311
|
+
lines.push(
|
|
312
|
+
` Maina — bugs introduced: ${mt.bugsIntroduced}, bugs caught: ${mt.bugsCaught}, tests: ${mt.testsPassed}/${mt.testsTotal}`,
|
|
313
|
+
);
|
|
314
|
+
lines.push(
|
|
315
|
+
` SpecKit — bugs introduced: ${st.bugsIntroduced}, bugs caught: ${st.bugsCaught}, tests: ${st.testsPassed}/${st.testsTotal}`,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push(` Winner: ${results.winner}`);
|
|
320
|
+
|
|
321
|
+
if (results.learnings.length > 0) {
|
|
322
|
+
lines.push("");
|
|
323
|
+
lines.push(" Learnings:");
|
|
324
|
+
for (const learning of results.learnings) {
|
|
325
|
+
lines.push(` - ${learning}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
lines.push("");
|
|
330
|
+
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Result } from "../db/index";
|
|
2
|
+
import type { BenchmarkMetrics } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface TestResult {
|
|
5
|
+
passed: number;
|
|
6
|
+
failed: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RunBenchmarkOptions {
|
|
11
|
+
pipeline: "maina" | "speckit";
|
|
12
|
+
storyName: string;
|
|
13
|
+
testFiles: string[];
|
|
14
|
+
implDir: string;
|
|
15
|
+
tokensInput?: number;
|
|
16
|
+
tokensOutput?: number;
|
|
17
|
+
verifyFindings?: number;
|
|
18
|
+
specQualityScore?: number;
|
|
19
|
+
implLOC?: number;
|
|
20
|
+
attemptsToPass?: number;
|
|
21
|
+
bugsIntroduced?: number;
|
|
22
|
+
toolsUsed?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse bun test stdout to extract pass/fail counts.
|
|
27
|
+
*/
|
|
28
|
+
export function parseTestOutput(output: string): TestResult {
|
|
29
|
+
const passMatch = output.match(/(\d+)\s+pass/);
|
|
30
|
+
const failMatch = output.match(/(\d+)\s+fail/);
|
|
31
|
+
|
|
32
|
+
const passed = passMatch ? Number.parseInt(passMatch[1] as string, 10) : 0;
|
|
33
|
+
const failed = failMatch ? Number.parseInt(failMatch[1] as string, 10) : 0;
|
|
34
|
+
|
|
35
|
+
return { passed, failed, total: passed + failed };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run benchmark tests against an implementation directory.
|
|
40
|
+
* Spawns `bun test` on the provided test files and captures metrics.
|
|
41
|
+
*/
|
|
42
|
+
export async function runBenchmark(
|
|
43
|
+
options: RunBenchmarkOptions,
|
|
44
|
+
): Promise<Result<BenchmarkMetrics>> {
|
|
45
|
+
const startMs = performance.now();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const proc = Bun.spawn(["bun", "test", ...options.testFiles], {
|
|
49
|
+
cwd: options.implDir,
|
|
50
|
+
stdout: "pipe",
|
|
51
|
+
stderr: "pipe",
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
MITT_IMPL_PATH: options.implDir,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const stdout = await new Response(proc.stdout).text();
|
|
59
|
+
const stderr = await new Response(proc.stderr).text();
|
|
60
|
+
await proc.exited;
|
|
61
|
+
|
|
62
|
+
const combined = stdout + stderr;
|
|
63
|
+
const testResult = parseTestOutput(combined);
|
|
64
|
+
const wallClockMs = Math.round(performance.now() - startMs);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
value: {
|
|
69
|
+
pipeline: options.pipeline,
|
|
70
|
+
storyName: options.storyName,
|
|
71
|
+
wallClockMs,
|
|
72
|
+
tokensInput: options.tokensInput ?? 0,
|
|
73
|
+
tokensOutput: options.tokensOutput ?? 0,
|
|
74
|
+
testsTotal: testResult.total,
|
|
75
|
+
testsPassed: testResult.passed,
|
|
76
|
+
testsFailed: testResult.failed,
|
|
77
|
+
verifyFindings: options.verifyFindings ?? 0,
|
|
78
|
+
specQualityScore: options.specQualityScore ?? 0,
|
|
79
|
+
implLOC: options.implLOC ?? 0,
|
|
80
|
+
attemptsToPass: options.attemptsToPass ?? 1,
|
|
81
|
+
bugsIntroduced: options.bugsIntroduced ?? 0,
|
|
82
|
+
toolsUsed: options.toolsUsed ?? [],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `Benchmark run failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Result } from "../db/index";
|
|
4
|
+
import type { LoadedStory, StoryConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List all available benchmark stories in the given directory.
|
|
8
|
+
* Each story must have a valid story.json to be included.
|
|
9
|
+
*/
|
|
10
|
+
export function listStories(storiesDir: string): Result<StoryConfig[]> {
|
|
11
|
+
if (!existsSync(storiesDir)) {
|
|
12
|
+
return { ok: true, value: [] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const entries = readdirSync(storiesDir, { withFileTypes: true });
|
|
16
|
+
const stories: StoryConfig[] = [];
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isDirectory()) continue;
|
|
20
|
+
|
|
21
|
+
const configPath = join(storiesDir, entry.name, "story.json");
|
|
22
|
+
if (!existsSync(configPath)) continue;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
26
|
+
const config = JSON.parse(raw) as StoryConfig;
|
|
27
|
+
if (config.name && config.description) {
|
|
28
|
+
stories.push(config);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Skip invalid configs
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ok: true, value: stories };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load a specific story by name, including its config, spec, and test files.
|
|
40
|
+
*/
|
|
41
|
+
export function loadStory(
|
|
42
|
+
storiesDir: string,
|
|
43
|
+
name: string,
|
|
44
|
+
): Result<LoadedStory> {
|
|
45
|
+
const storyDir = join(storiesDir, name);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(storyDir)) {
|
|
48
|
+
return { ok: false, error: `Story not found: ${name}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Load config
|
|
52
|
+
const configPath = join(storyDir, "story.json");
|
|
53
|
+
if (!existsSync(configPath)) {
|
|
54
|
+
return { ok: false, error: `Missing story.json in ${name}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let config: StoryConfig;
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
60
|
+
config = JSON.parse(raw) as StoryConfig;
|
|
61
|
+
} catch {
|
|
62
|
+
return { ok: false, error: `Invalid story.json in ${name}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Load spec
|
|
66
|
+
const specPath = join(storyDir, "spec.md");
|
|
67
|
+
if (!existsSync(specPath)) {
|
|
68
|
+
return { ok: false, error: `Missing spec.md in ${name}` };
|
|
69
|
+
}
|
|
70
|
+
const specContent = readFileSync(specPath, "utf-8");
|
|
71
|
+
|
|
72
|
+
// Load test files
|
|
73
|
+
const testFiles: Array<{ name: string; content: string }> = [];
|
|
74
|
+
for (const testFile of config.testFiles) {
|
|
75
|
+
const testPath = join(storyDir, testFile);
|
|
76
|
+
if (existsSync(testPath)) {
|
|
77
|
+
testFiles.push({
|
|
78
|
+
name: testFile,
|
|
79
|
+
content: readFileSync(testPath, "utf-8"),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
value: { config, specContent, testFiles, storyDir },
|
|
87
|
+
};
|
|
88
|
+
}
|