@mainahq/core 0.2.0 → 0.4.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.
Files changed (38) hide show
  1. package/package.json +4 -1
  2. package/src/ai/__tests__/delegation.test.ts +105 -0
  3. package/src/ai/delegation.ts +111 -0
  4. package/src/ai/try-generate.ts +17 -0
  5. package/src/cloud/__tests__/auth.test.ts +164 -0
  6. package/src/cloud/__tests__/client.test.ts +253 -0
  7. package/src/cloud/auth.ts +232 -0
  8. package/src/cloud/client.ts +190 -0
  9. package/src/cloud/types.ts +106 -0
  10. package/src/context/relevance.ts +5 -0
  11. package/src/context/retrieval.ts +3 -0
  12. package/src/context/semantic.ts +3 -0
  13. package/src/feedback/__tests__/trace-analysis.test.ts +98 -0
  14. package/src/feedback/trace-analysis.ts +153 -0
  15. package/src/index.ts +55 -0
  16. package/src/init/__tests__/init.test.ts +51 -0
  17. package/src/init/index.ts +43 -0
  18. package/src/language/__tests__/detect.test.ts +61 -1
  19. package/src/language/__tests__/profile.test.ts +68 -1
  20. package/src/language/detect.ts +33 -3
  21. package/src/language/profile.ts +67 -2
  22. package/src/ticket/index.ts +5 -0
  23. package/src/verify/__tests__/consistency.test.ts +98 -0
  24. package/src/verify/__tests__/lighthouse.test.ts +215 -0
  25. package/src/verify/__tests__/linters/checkstyle.test.ts +23 -0
  26. package/src/verify/__tests__/linters/dotnet-format.test.ts +18 -0
  27. package/src/verify/__tests__/pipeline.test.ts +21 -2
  28. package/src/verify/__tests__/typecheck.test.ts +160 -0
  29. package/src/verify/__tests__/zap.test.ts +188 -0
  30. package/src/verify/consistency.ts +199 -0
  31. package/src/verify/detect.ts +13 -1
  32. package/src/verify/lighthouse.ts +173 -0
  33. package/src/verify/linters/checkstyle.ts +41 -0
  34. package/src/verify/linters/dotnet-format.ts +37 -0
  35. package/src/verify/pipeline.ts +20 -2
  36. package/src/verify/syntax-guard.ts +8 -0
  37. package/src/verify/typecheck.ts +178 -0
  38. package/src/verify/zap.ts +189 -0
@@ -19,6 +19,7 @@ import { detectLanguages } from "../language/detect";
19
19
  import type { LanguageId } from "../language/profile";
20
20
  import { getProfile } from "../language/profile";
21
21
  import { type AIReviewResult, runAIReview } from "./ai-review";
22
+ import { checkConsistency } from "./consistency";
22
23
  import { runCoverage } from "./coverage";
23
24
  import type { DetectedTool } from "./detect";
24
25
  import { detectTools } from "./detect";
@@ -32,6 +33,7 @@ import { runSonar } from "./sonar";
32
33
  import type { SyntaxDiagnostic } from "./syntax-guard";
33
34
  import { syntaxGuard } from "./syntax-guard";
34
35
  import { runTrivy } from "./trivy";
36
+ import { runTypecheck } from "./typecheck";
35
37
 
36
38
  // ─── Types ────────────────────────────────────────────────────────────────
37
39
 
@@ -223,10 +225,26 @@ export async function runPipeline(
223
225
  ),
224
226
  );
225
227
 
228
+ // Built-in checks (always run, no external tool dependency)
229
+ toolPromises.push(
230
+ runToolWithTiming("typecheck", async () => {
231
+ const result = await runTypecheck(files, cwd, { language: primaryLang });
232
+ return { findings: result.findings, skipped: result.skipped };
233
+ }),
234
+ );
235
+
236
+ toolPromises.push(
237
+ runToolWithTiming("consistency", async () => {
238
+ const result = await checkConsistency(files, cwd, mainaDir);
239
+ return { findings: result.findings, skipped: false };
240
+ }),
241
+ );
242
+
226
243
  const toolReports = await Promise.all(toolPromises);
227
244
 
228
245
  // ── Step 4b: Warn if all external tools were skipped ─────────────────
229
- const externalTools = toolReports.filter((r) => r.tool !== "slop");
246
+ const builtInTools = new Set(["slop", "typecheck", "consistency"]);
247
+ const externalTools = toolReports.filter((r) => !builtInTools.has(r.tool));
230
248
  const allExternalSkipped =
231
249
  externalTools.length > 0 && externalTools.every((r) => r.skipped);
232
250
 
@@ -242,7 +260,7 @@ export async function runPipeline(
242
260
  tool: "pipeline",
243
261
  file: "",
244
262
  line: 0,
245
- message: `No external verification tools ran (${skippedNames} skipped). Run \`maina doctor\` to check tool health or \`maina init\` to configure.`,
263
+ message: `WARNING: No external verification tools detected (${skippedNames} skipped). Built-in checks (typecheck, consistency, slop) still ran. Run \`maina init --install\` to add external tools.`,
246
264
  severity: "warning",
247
265
  });
248
266
  }
@@ -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");
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Built-in Type Checking — runs language-native type checkers as a verify step.
3
+ *
4
+ * Zero external tool install required for TypeScript projects (uses project's tsc).
5
+ * For other languages: mypy (Python), go vet (Go), cargo check (Rust),
6
+ * dotnet build (C#), javac (Java).
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { LanguageId } from "../language/profile";
12
+ import type { Finding } from "./diff-filter";
13
+
14
+ // ─── Types ────────────────────────────────────────────────────────────────
15
+
16
+ export interface TypecheckResult {
17
+ findings: Finding[];
18
+ duration: number;
19
+ tool: string;
20
+ skipped: boolean;
21
+ }
22
+
23
+ export interface TypecheckCommand {
24
+ tool: string;
25
+ command: string;
26
+ args: string[];
27
+ configFile?: string;
28
+ }
29
+
30
+ // ─── Language-specific commands ───────────────────────────────────────────
31
+
32
+ const TYPECHECK_COMMANDS: Record<LanguageId, TypecheckCommand> = {
33
+ typescript: {
34
+ tool: "tsc",
35
+ command: "tsc",
36
+ args: ["--noEmit", "--pretty", "false"],
37
+ configFile: "tsconfig.json",
38
+ },
39
+ python: {
40
+ tool: "mypy",
41
+ command: "mypy",
42
+ args: ["--no-color-output", "--no-error-summary"],
43
+ },
44
+ go: {
45
+ tool: "go-vet",
46
+ command: "go",
47
+ args: ["vet", "./..."],
48
+ },
49
+ rust: {
50
+ tool: "cargo-check",
51
+ command: "cargo",
52
+ args: ["check", "--message-format=short"],
53
+ },
54
+ csharp: {
55
+ tool: "dotnet-build",
56
+ command: "dotnet",
57
+ args: ["build", "--no-restore", "--verbosity", "quiet"],
58
+ },
59
+ java: {
60
+ tool: "javac",
61
+ command: "javac",
62
+ args: ["-Xlint:all"],
63
+ },
64
+ php: {
65
+ tool: "phpstan",
66
+ command: "phpstan",
67
+ args: ["analyse", "--error-format=json", "--no-progress"],
68
+ },
69
+ };
70
+
71
+ export function getTypecheckCommand(language: LanguageId): TypecheckCommand {
72
+ return TYPECHECK_COMMANDS[language];
73
+ }
74
+
75
+ // ─── TSC Output Parser ───────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Parse tsc --noEmit --pretty false output into Finding[].
79
+ *
80
+ * Format: file(line,col): error TSxxxx: message
81
+ */
82
+ export function parseTscOutput(output: string): Finding[] {
83
+ if (!output.trim()) return [];
84
+
85
+ const findings: Finding[] = [];
86
+ const pattern =
87
+ /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
88
+
89
+ for (const match of output.matchAll(pattern)) {
90
+ findings.push({
91
+ tool: "tsc",
92
+ file: match[1] ?? "",
93
+ line: Number.parseInt(match[2] ?? "0", 10),
94
+ column: Number.parseInt(match[3] ?? "0", 10),
95
+ message: `${match[5] ?? ""}: ${match[6] ?? ""}`,
96
+ severity: match[4] === "error" ? "error" : "warning",
97
+ ruleId: match[5] ?? "",
98
+ });
99
+ }
100
+
101
+ return findings;
102
+ }
103
+
104
+ // ─── Runner ──────────────────────────────────────────────────────────────
105
+
106
+ export async function runTypecheck(
107
+ files: string[],
108
+ cwd: string,
109
+ options?: { command?: string; language?: LanguageId },
110
+ ): Promise<TypecheckResult> {
111
+ const language = options?.language ?? "typescript";
112
+ const cmd = TYPECHECK_COMMANDS[language];
113
+ const start = performance.now();
114
+
115
+ // Check config file exists (e.g., tsconfig.json for TS)
116
+ if (cmd.configFile && !existsSync(join(cwd, cmd.configFile))) {
117
+ return {
118
+ findings: [],
119
+ duration: performance.now() - start,
120
+ tool: cmd.tool,
121
+ skipped: true,
122
+ };
123
+ }
124
+
125
+ // Resolve command: check node_modules/.bin first (for tsc, mypy, etc.)
126
+ const localBin = join(cwd, "node_modules", ".bin", cmd.command);
127
+ const command =
128
+ options?.command ?? (existsSync(localBin) ? localBin : cmd.command);
129
+
130
+ try {
131
+ const proc = Bun.spawn([command, ...cmd.args], {
132
+ cwd,
133
+ stdout: "pipe",
134
+ stderr: "pipe",
135
+ env: { ...process.env, NO_COLOR: "1" },
136
+ });
137
+
138
+ const stdout = await new Response(proc.stdout).text();
139
+ const stderr = await new Response(proc.stderr).text();
140
+ await proc.exited;
141
+
142
+ const output = stdout + stderr;
143
+ let findings: Finding[];
144
+
145
+ if (language === "typescript") {
146
+ findings = parseTscOutput(output);
147
+ } else {
148
+ // For other languages, treat any non-zero exit as a generic finding
149
+ findings =
150
+ proc.exitCode !== 0 && output.trim()
151
+ ? [
152
+ {
153
+ tool: cmd.tool,
154
+ file: files[0] ?? "unknown",
155
+ line: 1,
156
+ message: output.trim().split("\n")[0] ?? "Type check failed",
157
+ severity: "error" as const,
158
+ },
159
+ ]
160
+ : [];
161
+ }
162
+
163
+ return {
164
+ findings,
165
+ duration: performance.now() - start,
166
+ tool: cmd.tool,
167
+ skipped: false,
168
+ };
169
+ } catch {
170
+ // Command not found or other spawn error
171
+ return {
172
+ findings: [],
173
+ duration: performance.now() - start,
174
+ tool: cmd.tool,
175
+ skipped: true,
176
+ };
177
+ }
178
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ZAP DAST Integration for the Verify Engine.
3
+ *
4
+ * Runs OWASP ZAP baseline scan via Docker against a target URL.
5
+ * Parses JSON output into the unified Finding type.
6
+ * Gracefully skips if Docker is not available or no target URL configured.
7
+ */
8
+
9
+ import { isToolAvailable } from "./detect";
10
+ import type { Finding } from "./diff-filter";
11
+
12
+ // ─── Types ────────────────────────────────────────────────────────────────
13
+
14
+ export interface ZapOptions {
15
+ targetUrl: string;
16
+ cwd: string;
17
+ /** Pre-resolved availability — skips redundant detection if provided. */
18
+ available?: boolean;
19
+ }
20
+
21
+ export interface ZapResult {
22
+ findings: Finding[];
23
+ skipped: boolean;
24
+ }
25
+
26
+ // ─── JSON Parsing ─────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Map ZAP risk description to unified severity.
30
+ *
31
+ * ZAP riskdesc format: "High (Medium)", "Medium (Low)", "Low (Medium)", etc.
32
+ * We extract the first word (risk level) to map severity.
33
+ */
34
+ function mapZapRisk(riskdesc: string): "error" | "warning" | "info" {
35
+ const risk = riskdesc.split(" ")[0]?.toLowerCase() ?? "";
36
+ switch (risk) {
37
+ case "high":
38
+ return "error";
39
+ case "medium":
40
+ return "warning";
41
+ case "low":
42
+ case "informational":
43
+ return "info";
44
+ default:
45
+ return "warning";
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Parse ZAP JSON output into Finding[].
51
+ *
52
+ * ZAP JSON baseline report has this structure:
53
+ * ```json
54
+ * {
55
+ * "site": [{
56
+ * "alerts": [{
57
+ * "pluginid": "10021",
58
+ * "alert": "X-Content-Type-Options Header Missing",
59
+ * "riskdesc": "Low (Medium)",
60
+ * "desc": "The Anti-MIME-Sniffing header is not set.",
61
+ * "instances": [{
62
+ * "uri": "https://example.com/api/health",
63
+ * "method": "GET"
64
+ * }]
65
+ * }]
66
+ * }]
67
+ * }
68
+ * ```
69
+ *
70
+ * Each alert may have multiple instances (URLs). We create one Finding per
71
+ * instance. If an alert has no instances, we still emit one Finding with
72
+ * an empty file.
73
+ *
74
+ * Handles malformed JSON and unexpected structures gracefully.
75
+ */
76
+ export function parseZapJson(json: string): Finding[] {
77
+ let parsed: Record<string, unknown>;
78
+ try {
79
+ parsed = JSON.parse(json) as Record<string, unknown>;
80
+ } catch {
81
+ return [];
82
+ }
83
+
84
+ const sites = parsed.site;
85
+ if (!Array.isArray(sites)) {
86
+ return [];
87
+ }
88
+
89
+ const findings: Finding[] = [];
90
+
91
+ for (const site of sites) {
92
+ const s = site as Record<string, unknown>;
93
+ const alerts = s.alerts;
94
+
95
+ if (!Array.isArray(alerts)) {
96
+ continue;
97
+ }
98
+
99
+ for (const alert of alerts) {
100
+ const a = alert as Record<string, unknown>;
101
+ const pluginId = (a.pluginid as string) ?? undefined;
102
+ const alertName = (a.alert as string) ?? "";
103
+ const riskdesc = (a.riskdesc as string) ?? "Informational";
104
+ const desc = (a.desc as string) ?? "";
105
+ const instances = a.instances;
106
+ const severity = mapZapRisk(riskdesc);
107
+
108
+ const message = `${alertName}: ${desc}`;
109
+
110
+ if (Array.isArray(instances) && instances.length > 0) {
111
+ for (const instance of instances) {
112
+ const inst = instance as Record<string, unknown>;
113
+ const uri = (inst.uri as string) ?? "";
114
+
115
+ findings.push({
116
+ tool: "zap",
117
+ file: uri,
118
+ line: 0,
119
+ message,
120
+ severity,
121
+ ruleId: pluginId,
122
+ });
123
+ }
124
+ } else {
125
+ findings.push({
126
+ tool: "zap",
127
+ file: "",
128
+ line: 0,
129
+ message,
130
+ severity,
131
+ ruleId: pluginId,
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ return findings;
138
+ }
139
+
140
+ // ─── Runner ───────────────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Run ZAP baseline scan via Docker and return parsed findings.
144
+ *
145
+ * If Docker is not available or no targetUrl is configured,
146
+ * returns `{ findings: [], skipped: true }`.
147
+ * If ZAP fails, returns `{ findings: [], skipped: false }`.
148
+ */
149
+ export async function runZap(options: ZapOptions): Promise<ZapResult> {
150
+ if (!options.targetUrl) {
151
+ return { findings: [], skipped: true };
152
+ }
153
+
154
+ const toolAvailable = options.available ?? (await isToolAvailable("zap"));
155
+ if (!toolAvailable) {
156
+ return { findings: [], skipped: true };
157
+ }
158
+
159
+ const cwd = options.cwd;
160
+
161
+ const args = [
162
+ "docker",
163
+ "run",
164
+ "--rm",
165
+ "zaproxy/zap-stable",
166
+ "zap-baseline.py",
167
+ "-t",
168
+ options.targetUrl,
169
+ "-J",
170
+ "report.json",
171
+ ];
172
+
173
+ try {
174
+ const proc = Bun.spawn(args, {
175
+ cwd,
176
+ stdout: "pipe",
177
+ stderr: "pipe",
178
+ });
179
+
180
+ const stdout = await new Response(proc.stdout).text();
181
+ await new Response(proc.stderr).text();
182
+ await proc.exited;
183
+
184
+ const findings = parseZapJson(stdout);
185
+ return { findings, skipped: false };
186
+ } catch {
187
+ return { findings: [], skipped: false };
188
+ }
189
+ }