@mainahq/core 0.2.0 → 0.3.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__/delegation.test.ts +105 -0
- package/src/ai/delegation.ts +111 -0
- package/src/ai/try-generate.ts +17 -0
- package/src/context/relevance.ts +5 -0
- package/src/context/retrieval.ts +3 -0
- package/src/context/semantic.ts +3 -0
- package/src/index.ts +9 -0
- package/src/language/__tests__/profile.test.ts +15 -0
- package/src/language/detect.ts +15 -1
- package/src/language/profile.ts +44 -1
- package/src/verify/__tests__/linters/checkstyle.test.ts +23 -0
- package/src/verify/__tests__/linters/dotnet-format.test.ts +18 -0
- package/src/verify/detect.ts +9 -1
- package/src/verify/linters/checkstyle.ts +41 -0
- package/src/verify/linters/dotnet-format.ts +37 -0
- package/src/verify/syntax-guard.ts +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type DelegationRequest,
|
|
4
|
+
formatDelegationRequest,
|
|
5
|
+
parseDelegationRequest,
|
|
6
|
+
} from "../delegation";
|
|
7
|
+
|
|
8
|
+
describe("formatDelegationRequest", () => {
|
|
9
|
+
it("should format a request with all fields", () => {
|
|
10
|
+
const req: DelegationRequest = {
|
|
11
|
+
task: "ai-review",
|
|
12
|
+
context: "Reviewing diff for cross-function consistency",
|
|
13
|
+
prompt: "Review this diff:\n+const x = 1;",
|
|
14
|
+
expectedFormat: "json",
|
|
15
|
+
schema: '{"findings":[]}',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const formatted = formatDelegationRequest(req);
|
|
19
|
+
|
|
20
|
+
expect(formatted).toContain("---MAINA_AI_REQUEST---");
|
|
21
|
+
expect(formatted).toContain("---END_MAINA_AI_REQUEST---");
|
|
22
|
+
expect(formatted).toContain("task: ai-review");
|
|
23
|
+
expect(formatted).toContain("context: Reviewing diff");
|
|
24
|
+
expect(formatted).toContain("expected_format: json");
|
|
25
|
+
expect(formatted).toContain("schema: ");
|
|
26
|
+
expect(formatted).toContain("prompt: |");
|
|
27
|
+
expect(formatted).toContain(" Review this diff:");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should format without optional schema", () => {
|
|
31
|
+
const req: DelegationRequest = {
|
|
32
|
+
task: "commit-msg",
|
|
33
|
+
context: "Generate commit message",
|
|
34
|
+
prompt: "Generate a commit message for these changes",
|
|
35
|
+
expectedFormat: "text",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatted = formatDelegationRequest(req);
|
|
39
|
+
expect(formatted).not.toContain("schema:");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("parseDelegationRequest", () => {
|
|
44
|
+
it("should parse a formatted request back", () => {
|
|
45
|
+
const original: DelegationRequest = {
|
|
46
|
+
task: "ai-review",
|
|
47
|
+
context: "Reviewing diff",
|
|
48
|
+
prompt: "Review this diff:\n+const x = 1;",
|
|
49
|
+
expectedFormat: "json",
|
|
50
|
+
schema: '{"findings":[]}',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const formatted = formatDelegationRequest(original);
|
|
54
|
+
const parsed = parseDelegationRequest(formatted);
|
|
55
|
+
|
|
56
|
+
expect(parsed).not.toBeNull();
|
|
57
|
+
expect(parsed?.task).toBe("ai-review");
|
|
58
|
+
expect(parsed?.context).toBe("Reviewing diff");
|
|
59
|
+
expect(parsed?.expectedFormat).toBe("json");
|
|
60
|
+
expect(parsed?.schema).toBe('{"findings":[]}');
|
|
61
|
+
expect(parsed?.prompt).toContain("Review this diff:");
|
|
62
|
+
expect(parsed?.prompt).toContain("+const x = 1;");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should return null for text without markers", () => {
|
|
66
|
+
expect(parseDelegationRequest("no markers here")).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should return null for empty task", () => {
|
|
70
|
+
const text =
|
|
71
|
+
"---MAINA_AI_REQUEST---\ncontext: test\n---END_MAINA_AI_REQUEST---";
|
|
72
|
+
expect(parseDelegationRequest(text)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should handle multiline prompts", () => {
|
|
76
|
+
const req: DelegationRequest = {
|
|
77
|
+
task: "review",
|
|
78
|
+
context: "Code review",
|
|
79
|
+
prompt: "Line 1\nLine 2\nLine 3",
|
|
80
|
+
expectedFormat: "markdown",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const formatted = formatDelegationRequest(req);
|
|
84
|
+
const parsed = parseDelegationRequest(formatted);
|
|
85
|
+
|
|
86
|
+
expect(parsed?.prompt).toContain("Line 1");
|
|
87
|
+
expect(parsed?.prompt).toContain("Line 2");
|
|
88
|
+
expect(parsed?.prompt).toContain("Line 3");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should parse request embedded in other text", () => {
|
|
92
|
+
const text = `Some output before
|
|
93
|
+
---MAINA_AI_REQUEST---
|
|
94
|
+
task: test
|
|
95
|
+
context: testing
|
|
96
|
+
prompt: |
|
|
97
|
+
hello
|
|
98
|
+
---END_MAINA_AI_REQUEST---
|
|
99
|
+
Some output after`;
|
|
100
|
+
|
|
101
|
+
const parsed = parseDelegationRequest(text);
|
|
102
|
+
expect(parsed?.task).toBe("test");
|
|
103
|
+
expect(parsed?.prompt).toBe("hello");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Delegation Protocol — structured stdout protocol for host agents.
|
|
3
|
+
*
|
|
4
|
+
* When maina runs inside Claude Code/Codex/OpenCode without an API key,
|
|
5
|
+
* AI-dependent steps output request blocks that the host agent can
|
|
6
|
+
* parse and process with its own AI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface DelegationRequest {
|
|
12
|
+
task: string;
|
|
13
|
+
context: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
expectedFormat: "json" | "markdown" | "text";
|
|
16
|
+
schema?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const START_MARKER = "---MAINA_AI_REQUEST---";
|
|
22
|
+
const END_MARKER = "---END_MAINA_AI_REQUEST---";
|
|
23
|
+
|
|
24
|
+
// ─── Format ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a delegation request as a structured text block.
|
|
28
|
+
* This block is output to stdout for host agents to parse.
|
|
29
|
+
*/
|
|
30
|
+
export function formatDelegationRequest(req: DelegationRequest): string {
|
|
31
|
+
const lines: string[] = [START_MARKER];
|
|
32
|
+
lines.push(`task: ${req.task}`);
|
|
33
|
+
lines.push(`context: ${req.context}`);
|
|
34
|
+
lines.push(`expected_format: ${req.expectedFormat}`);
|
|
35
|
+
if (req.schema) {
|
|
36
|
+
lines.push(`schema: ${req.schema}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("prompt: |");
|
|
39
|
+
// Indent prompt lines by 2 spaces (YAML-style block scalar)
|
|
40
|
+
for (const line of req.prompt.split("\n")) {
|
|
41
|
+
lines.push(` ${line}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(END_MARKER);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Parse ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a delegation request block from text.
|
|
51
|
+
* Returns null if the text doesn't contain a valid request block.
|
|
52
|
+
*/
|
|
53
|
+
export function parseDelegationRequest(text: string): DelegationRequest | null {
|
|
54
|
+
const startIdx = text.indexOf(START_MARKER);
|
|
55
|
+
const endIdx = text.indexOf(END_MARKER);
|
|
56
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const block = text.slice(startIdx + START_MARKER.length, endIdx).trim();
|
|
61
|
+
const lines = block.split("\n");
|
|
62
|
+
|
|
63
|
+
let task = "";
|
|
64
|
+
let context = "";
|
|
65
|
+
let expectedFormat: DelegationRequest["expectedFormat"] = "text";
|
|
66
|
+
let schema: string | undefined;
|
|
67
|
+
const promptLines: string[] = [];
|
|
68
|
+
let inPrompt = false;
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (inPrompt) {
|
|
72
|
+
// Prompt lines are indented by 2 spaces
|
|
73
|
+
promptLines.push(line.startsWith(" ") ? line.slice(2) : line);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (line.startsWith("task: ")) {
|
|
78
|
+
task = line.slice(6).trim();
|
|
79
|
+
} else if (line.startsWith("context: ")) {
|
|
80
|
+
context = line.slice(9).trim();
|
|
81
|
+
} else if (line.startsWith("expected_format: ")) {
|
|
82
|
+
const fmt = line.slice(17).trim();
|
|
83
|
+
if (fmt === "json" || fmt === "markdown" || fmt === "text") {
|
|
84
|
+
expectedFormat = fmt;
|
|
85
|
+
}
|
|
86
|
+
} else if (line.startsWith("schema: ")) {
|
|
87
|
+
schema = line.slice(8).trim();
|
|
88
|
+
} else if (line.startsWith("prompt: |")) {
|
|
89
|
+
inPrompt = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!task) return null;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
task,
|
|
97
|
+
context,
|
|
98
|
+
prompt: promptLines.join("\n").trim(),
|
|
99
|
+
expectedFormat,
|
|
100
|
+
schema,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Output a delegation request to stdout.
|
|
106
|
+
* Used by tryAIGenerate when in host mode.
|
|
107
|
+
*/
|
|
108
|
+
export function outputDelegationRequest(req: DelegationRequest): void {
|
|
109
|
+
const formatted = formatDelegationRequest(req);
|
|
110
|
+
process.stdout.write(`\n${formatted}\n`);
|
|
111
|
+
}
|
package/src/ai/try-generate.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getApiKey, isHostMode } from "../config/index";
|
|
2
|
+
import { outputDelegationRequest } from "./delegation";
|
|
2
3
|
|
|
3
4
|
export interface DelegationPrompt {
|
|
4
5
|
task: string;
|
|
@@ -69,6 +70,22 @@ export async function tryAIGenerate(
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// Output structured request for host agent to process
|
|
74
|
+
outputDelegationRequest({
|
|
75
|
+
task,
|
|
76
|
+
context: `AI ${task} requested — process with host AI`,
|
|
77
|
+
prompt: userPrompt,
|
|
78
|
+
expectedFormat:
|
|
79
|
+
task === "commit"
|
|
80
|
+
? "text"
|
|
81
|
+
: task.includes("review")
|
|
82
|
+
? "json"
|
|
83
|
+
: "markdown",
|
|
84
|
+
schema: task.includes("review")
|
|
85
|
+
? '{"findings":[{"file":"path","line":42,"message":"desc","severity":"warning"}]}'
|
|
86
|
+
: undefined,
|
|
87
|
+
});
|
|
88
|
+
|
|
72
89
|
// Host mode — return structured delegation for host agent to process
|
|
73
90
|
return {
|
|
74
91
|
text: null,
|
package/src/context/relevance.ts
CHANGED
package/src/context/retrieval.ts
CHANGED
|
@@ -236,6 +236,9 @@ export async function searchWithGrep(
|
|
|
236
236
|
"--include=*.py",
|
|
237
237
|
"--include=*.go",
|
|
238
238
|
"--include=*.rs",
|
|
239
|
+
"--include=*.cs",
|
|
240
|
+
"--include=*.java",
|
|
241
|
+
"--include=*.kt",
|
|
239
242
|
"--exclude-dir=node_modules",
|
|
240
243
|
"--exclude-dir=dist",
|
|
241
244
|
"--exclude-dir=.git",
|
package/src/context/semantic.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,6 +2,13 @@ export const VERSION = "0.1.0";
|
|
|
2
2
|
|
|
3
3
|
// AI
|
|
4
4
|
export { generateCommitMessage } from "./ai/commit-msg";
|
|
5
|
+
// AI — Delegation
|
|
6
|
+
export {
|
|
7
|
+
type DelegationRequest,
|
|
8
|
+
formatDelegationRequest,
|
|
9
|
+
outputDelegationRequest,
|
|
10
|
+
parseDelegationRequest,
|
|
11
|
+
} from "./ai/delegation";
|
|
5
12
|
export {
|
|
6
13
|
type DesignApproach,
|
|
7
14
|
generateDesignApproaches,
|
|
@@ -171,9 +178,11 @@ export {
|
|
|
171
178
|
getPrimaryLanguage,
|
|
172
179
|
} from "./language/detect";
|
|
173
180
|
export {
|
|
181
|
+
CSHARP_PROFILE,
|
|
174
182
|
GO_PROFILE,
|
|
175
183
|
getProfile,
|
|
176
184
|
getSupportedLanguages,
|
|
185
|
+
JAVA_PROFILE,
|
|
177
186
|
type LanguageId,
|
|
178
187
|
type LanguageProfile,
|
|
179
188
|
PYTHON_PROFILE,
|
|
@@ -35,6 +35,21 @@ describe("LanguageProfile", () => {
|
|
|
35
35
|
expect(profile.commentPrefixes).toContain("//");
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it("should return C# profile", () => {
|
|
39
|
+
const profile = getProfile("csharp");
|
|
40
|
+
expect(profile.id).toBe("csharp");
|
|
41
|
+
expect(profile.extensions).toContain(".cs");
|
|
42
|
+
expect(profile.syntaxTool).toBe("dotnet-format");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return Java profile", () => {
|
|
46
|
+
const profile = getProfile("java");
|
|
47
|
+
expect(profile.id).toBe("java");
|
|
48
|
+
expect(profile.extensions).toContain(".java");
|
|
49
|
+
expect(profile.extensions).toContain(".kt");
|
|
50
|
+
expect(profile.syntaxTool).toBe("checkstyle");
|
|
51
|
+
});
|
|
52
|
+
|
|
38
53
|
it("should have test file pattern for each language", () => {
|
|
39
54
|
expect(TYPESCRIPT_PROFILE.testFilePattern.test("app.test.ts")).toBe(true);
|
|
40
55
|
expect(getProfile("python").testFilePattern.test("test_app.py")).toBe(true);
|
package/src/language/detect.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Language Detection — detects project languages from marker files.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import type { LanguageId } from "./profile";
|
|
8
8
|
|
|
@@ -17,6 +17,8 @@ const LANGUAGE_MARKERS: Record<LanguageId, string[]> = {
|
|
|
17
17
|
],
|
|
18
18
|
go: ["go.mod", "go.sum"],
|
|
19
19
|
rust: ["Cargo.toml", "Cargo.lock"],
|
|
20
|
+
csharp: ["global.json", "Directory.Build.props"],
|
|
21
|
+
java: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"],
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -39,6 +41,18 @@ export function detectLanguages(cwd: string): LanguageId[] {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
// C# — check for .sln or .csproj files (names vary)
|
|
45
|
+
if (!detected.includes("csharp")) {
|
|
46
|
+
try {
|
|
47
|
+
const entries = readdirSync(cwd);
|
|
48
|
+
if (
|
|
49
|
+
entries.some((e: string) => e.endsWith(".sln") || e.endsWith(".csproj"))
|
|
50
|
+
) {
|
|
51
|
+
detected.push("csharp");
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
// Also check package.json for TypeScript dependency
|
|
43
57
|
if (!detected.includes("typescript")) {
|
|
44
58
|
const pkgPath = join(cwd, "package.json");
|
package/src/language/profile.ts
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* file patterns, and slop detection rules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export type LanguageId =
|
|
6
|
+
export type LanguageId =
|
|
7
|
+
| "typescript"
|
|
8
|
+
| "python"
|
|
9
|
+
| "go"
|
|
10
|
+
| "rust"
|
|
11
|
+
| "csharp"
|
|
12
|
+
| "java";
|
|
7
13
|
|
|
8
14
|
export interface LanguageProfile {
|
|
9
15
|
id: LanguageId;
|
|
@@ -94,11 +100,48 @@ export const RUST_PROFILE: LanguageProfile = {
|
|
|
94
100
|
fileGlobs: ["*.rs"],
|
|
95
101
|
};
|
|
96
102
|
|
|
103
|
+
export const CSHARP_PROFILE: LanguageProfile = {
|
|
104
|
+
id: "csharp",
|
|
105
|
+
displayName: "C#",
|
|
106
|
+
extensions: [".cs"],
|
|
107
|
+
syntaxTool: "dotnet-format",
|
|
108
|
+
syntaxArgs: (files, _cwd) => [
|
|
109
|
+
"dotnet",
|
|
110
|
+
"format",
|
|
111
|
+
"--verify-no-changes",
|
|
112
|
+
"--include",
|
|
113
|
+
...files,
|
|
114
|
+
],
|
|
115
|
+
commentPrefixes: ["//", "/*"],
|
|
116
|
+
testFilePattern: /(?:Tests?\.cs$|\.Tests?\.|tests\/)/,
|
|
117
|
+
printPattern: /Console\.Write(?:Line)?\s*\(/,
|
|
118
|
+
lintIgnorePattern:
|
|
119
|
+
/#pragma\s+warning\s+disable|\/\/\s*noinspection|\[SuppressMessage/,
|
|
120
|
+
importPattern: /^using\s+(\S+)/,
|
|
121
|
+
fileGlobs: ["*.cs"],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const JAVA_PROFILE: LanguageProfile = {
|
|
125
|
+
id: "java",
|
|
126
|
+
displayName: "Java",
|
|
127
|
+
extensions: [".java", ".kt"],
|
|
128
|
+
syntaxTool: "checkstyle",
|
|
129
|
+
syntaxArgs: (files, _cwd) => ["checkstyle", "-f", "xml", ...files],
|
|
130
|
+
commentPrefixes: ["//", "/*"],
|
|
131
|
+
testFilePattern: /(?:Test\.java$|Spec\.java$|src\/test\/)/,
|
|
132
|
+
printPattern: /System\.out\.print(?:ln)?\s*\(/,
|
|
133
|
+
lintIgnorePattern: /@SuppressWarnings|\/\/\s*NOPMD|\/\/\s*NOSONAR/,
|
|
134
|
+
importPattern: /^import\s+(\S+)/,
|
|
135
|
+
fileGlobs: ["*.java", "*.kt"],
|
|
136
|
+
};
|
|
137
|
+
|
|
97
138
|
const PROFILES: Record<LanguageId, LanguageProfile> = {
|
|
98
139
|
typescript: TYPESCRIPT_PROFILE,
|
|
99
140
|
python: PYTHON_PROFILE,
|
|
100
141
|
go: GO_PROFILE,
|
|
101
142
|
rust: RUST_PROFILE,
|
|
143
|
+
csharp: CSHARP_PROFILE,
|
|
144
|
+
java: JAVA_PROFILE,
|
|
102
145
|
};
|
|
103
146
|
|
|
104
147
|
export function getProfile(id: LanguageId): LanguageProfile {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseCheckstyleOutput } from "../../linters/checkstyle";
|
|
3
|
+
|
|
4
|
+
describe("parseCheckstyleOutput", () => {
|
|
5
|
+
it("should parse checkstyle XML", () => {
|
|
6
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<checkstyle>
|
|
8
|
+
<file name="src/Main.java">
|
|
9
|
+
<error line="10" column="5" severity="error" message="Missing Javadoc comment" source="com.puppycrawl.tools.checkstyle.checks.javadoc"/>
|
|
10
|
+
<error line="20" severity="warning" message="Line is longer than 120 characters" source="com.puppycrawl.tools.checkstyle.checks.sizes"/>
|
|
11
|
+
</file>
|
|
12
|
+
</checkstyle>`;
|
|
13
|
+
const diagnostics = parseCheckstyleOutput(xml);
|
|
14
|
+
expect(diagnostics).toHaveLength(2);
|
|
15
|
+
expect(diagnostics[0]?.file).toBe("src/Main.java");
|
|
16
|
+
expect(diagnostics[0]?.severity).toBe("error");
|
|
17
|
+
expect(diagnostics[1]?.severity).toBe("warning");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should handle empty XML", () => {
|
|
21
|
+
expect(parseCheckstyleOutput("")).toHaveLength(0);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseDotnetFormatOutput } from "../../linters/dotnet-format";
|
|
3
|
+
|
|
4
|
+
describe("parseDotnetFormatOutput", () => {
|
|
5
|
+
it("should parse dotnet format diagnostics", () => {
|
|
6
|
+
const output = `Program.cs(10,5): warning CS1234: Unused variable 'x'
|
|
7
|
+
Service.cs(20,1): error CS0246: The type or namespace name 'Foo' could not be found`;
|
|
8
|
+
const diagnostics = parseDotnetFormatOutput(output);
|
|
9
|
+
expect(diagnostics).toHaveLength(2);
|
|
10
|
+
expect(diagnostics[0]?.file).toBe("Program.cs");
|
|
11
|
+
expect(diagnostics[0]?.severity).toBe("warning");
|
|
12
|
+
expect(diagnostics[1]?.severity).toBe("error");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should handle empty output", () => {
|
|
16
|
+
expect(parseDotnetFormatOutput("")).toHaveLength(0);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/verify/detect.ts
CHANGED
|
@@ -22,7 +22,11 @@ export type ToolName =
|
|
|
22
22
|
| "golangci-lint"
|
|
23
23
|
| "cargo-clippy"
|
|
24
24
|
| "cargo-audit"
|
|
25
|
-
| "playwright"
|
|
25
|
+
| "playwright"
|
|
26
|
+
| "dotnet-format"
|
|
27
|
+
| "checkstyle"
|
|
28
|
+
| "spotbugs"
|
|
29
|
+
| "pmd";
|
|
26
30
|
|
|
27
31
|
export interface DetectedTool {
|
|
28
32
|
name: string;
|
|
@@ -47,6 +51,10 @@ export const TOOL_REGISTRY: Record<
|
|
|
47
51
|
"cargo-clippy": { command: "cargo", versionFlag: "clippy --version" },
|
|
48
52
|
"cargo-audit": { command: "cargo-audit", versionFlag: "--version" },
|
|
49
53
|
playwright: { command: "npx", versionFlag: "playwright --version" },
|
|
54
|
+
"dotnet-format": { command: "dotnet", versionFlag: "format --version" },
|
|
55
|
+
checkstyle: { command: "checkstyle", versionFlag: "--version" },
|
|
56
|
+
spotbugs: { command: "spotbugs", versionFlag: "-version" },
|
|
57
|
+
pmd: { command: "pmd", versionFlag: "--version" },
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkstyle output parser for Java linting.
|
|
3
|
+
* Parses Checkstyle XML output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyntaxDiagnostic } from "../syntax-guard";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse Checkstyle XML output into SyntaxDiagnostic[].
|
|
10
|
+
*/
|
|
11
|
+
export function parseCheckstyleOutput(xml: string): SyntaxDiagnostic[] {
|
|
12
|
+
const diagnostics: SyntaxDiagnostic[] = [];
|
|
13
|
+
|
|
14
|
+
// Simple XML parsing — extract <error> elements
|
|
15
|
+
// Format: <file name="path"><error line="10" column="5" severity="error" message="desc" source="rule"/></file>
|
|
16
|
+
const filePattern = /<file\s+name="([^"]+)">([\s\S]*?)<\/file>/g;
|
|
17
|
+
const errorPattern =
|
|
18
|
+
/<error\s+line="(\d+)"\s+(?:column="(\d+)"\s+)?severity="(\w+)"\s+message="([^"]+)"/g;
|
|
19
|
+
|
|
20
|
+
for (const fileMatch of xml.matchAll(filePattern)) {
|
|
21
|
+
const filePath = fileMatch[1] ?? "";
|
|
22
|
+
const fileContent = fileMatch[2] ?? "";
|
|
23
|
+
|
|
24
|
+
for (const errorMatch of fileContent.matchAll(errorPattern)) {
|
|
25
|
+
const line = Number.parseInt(errorMatch[1] ?? "0", 10);
|
|
26
|
+
const column = Number.parseInt(errorMatch[2] ?? "0", 10);
|
|
27
|
+
const severity = errorMatch[3] ?? "warning";
|
|
28
|
+
const message = errorMatch[4] ?? "";
|
|
29
|
+
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
file: filePath,
|
|
32
|
+
line,
|
|
33
|
+
column,
|
|
34
|
+
message,
|
|
35
|
+
severity: severity === "error" ? "error" : "warning",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return diagnostics;
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dotnet format output parser for C# linting.
|
|
3
|
+
* Parses `dotnet format --verify-no-changes` output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyntaxDiagnostic } from "../syntax-guard";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse dotnet format text output into SyntaxDiagnostic[].
|
|
10
|
+
* dotnet format outputs: "path/file.cs(line,col): severity CODE: message"
|
|
11
|
+
*/
|
|
12
|
+
export function parseDotnetFormatOutput(output: string): SyntaxDiagnostic[] {
|
|
13
|
+
const diagnostics: SyntaxDiagnostic[] = [];
|
|
14
|
+
const lines = output.split("\n");
|
|
15
|
+
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (!line.trim()) continue;
|
|
18
|
+
// Format: file.cs(line,col): warning CS1234: message
|
|
19
|
+
const match = line.match(
|
|
20
|
+
/^(.+?)\((\d+),(\d+)\):\s*(error|warning)\s+(\w+):\s*(.+)$/,
|
|
21
|
+
);
|
|
22
|
+
if (!match) continue;
|
|
23
|
+
|
|
24
|
+
const [, file, lineStr, colStr, severity, _code, message] = match;
|
|
25
|
+
if (!file || !lineStr || !message) continue;
|
|
26
|
+
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
file,
|
|
29
|
+
line: Number.parseInt(lineStr, 10),
|
|
30
|
+
column: Number.parseInt(colStr ?? "0", 10),
|
|
31
|
+
message,
|
|
32
|
+
severity: severity === "error" ? "error" : "warning",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return diagnostics;
|
|
37
|
+
}
|
|
@@ -13,7 +13,9 @@ import { join } from "node:path";
|
|
|
13
13
|
import type { Result } from "../db/index";
|
|
14
14
|
import type { LanguageProfile } from "../language/profile";
|
|
15
15
|
import { TYPESCRIPT_PROFILE } from "../language/profile";
|
|
16
|
+
import { parseCheckstyleOutput } from "./linters/checkstyle";
|
|
16
17
|
import { parseClippyOutput } from "./linters/clippy";
|
|
18
|
+
import { parseDotnetFormatOutput } from "./linters/dotnet-format";
|
|
17
19
|
import { parseGoVetOutput } from "./linters/go-vet";
|
|
18
20
|
import { parseRuffOutput } from "./linters/ruff";
|
|
19
21
|
|
|
@@ -225,6 +227,12 @@ async function runLanguageLinter(
|
|
|
225
227
|
case "rust":
|
|
226
228
|
diagnostics = parseClippyOutput(stdout);
|
|
227
229
|
break;
|
|
230
|
+
case "csharp":
|
|
231
|
+
diagnostics = parseDotnetFormatOutput(stdout);
|
|
232
|
+
break;
|
|
233
|
+
case "java":
|
|
234
|
+
diagnostics = parseCheckstyleOutput(stdout);
|
|
235
|
+
break;
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
const errors = diagnostics.filter((d) => d.severity === "error");
|