@mainahq/core 0.3.0 → 0.5.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 +4 -1
- package/src/cloud/__tests__/auth.test.ts +164 -0
- package/src/cloud/__tests__/client.test.ts +253 -0
- package/src/cloud/auth.ts +232 -0
- package/src/cloud/client.ts +190 -0
- package/src/cloud/types.ts +106 -0
- package/src/feedback/__tests__/trace-analysis.test.ts +98 -0
- package/src/feedback/trace-analysis.ts +153 -0
- package/src/index.ts +46 -0
- package/src/init/__tests__/init.test.ts +51 -0
- package/src/init/index.ts +43 -0
- package/src/language/__tests__/detect.test.ts +61 -1
- package/src/language/__tests__/profile.test.ts +53 -1
- package/src/language/detect.ts +18 -2
- package/src/language/profile.ts +24 -2
- package/src/ticket/index.ts +5 -0
- package/src/verify/__tests__/consistency.test.ts +98 -0
- package/src/verify/__tests__/lighthouse.test.ts +215 -0
- package/src/verify/__tests__/pipeline.test.ts +21 -2
- package/src/verify/__tests__/typecheck.test.ts +160 -0
- package/src/verify/__tests__/zap.test.ts +188 -0
- package/src/verify/consistency.ts +199 -0
- package/src/verify/detect.ts +5 -1
- package/src/verify/lighthouse.ts +173 -0
- package/src/verify/pipeline.ts +20 -2
- package/src/verify/typecheck.ts +178 -0
- package/src/verify/zap.ts +189 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for built-in type checking in the verify pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that runTypecheck() spawns the correct language-specific
|
|
5
|
+
* type checker and parses its output into Finding[].
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
describe("runTypecheck", () => {
|
|
14
|
+
let testDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
testDir = join(tmpdir(), `maina-typecheck-test-${Date.now()}`);
|
|
18
|
+
mkdirSync(testDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should parse tsc --noEmit output into Finding[]", async () => {
|
|
26
|
+
// Create a tsconfig.json and install typescript locally
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(testDir, "tsconfig.json"),
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
compilerOptions: { strict: true, noEmit: true },
|
|
31
|
+
include: ["*.ts"],
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
// Install typescript locally so tsc is in node_modules/.bin
|
|
35
|
+
const install = Bun.spawnSync(["bun", "add", "typescript"], {
|
|
36
|
+
cwd: testDir,
|
|
37
|
+
});
|
|
38
|
+
if (install.exitCode !== 0) {
|
|
39
|
+
// Skip if we can't install (CI without network, etc.)
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create a file with a type error
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(testDir, "bad.ts"),
|
|
46
|
+
'const x: number = "not a number";\n',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const { runTypecheck } = await import("../typecheck");
|
|
50
|
+
const result = await runTypecheck(["bad.ts"], testDir);
|
|
51
|
+
|
|
52
|
+
expect(result.findings.length).toBeGreaterThan(0);
|
|
53
|
+
const first = result.findings[0];
|
|
54
|
+
if (!first) throw new Error("Expected at least one finding");
|
|
55
|
+
expect(first.tool).toBe("tsc");
|
|
56
|
+
expect(first.severity).toBe("error");
|
|
57
|
+
expect(first.file).toContain("bad.ts");
|
|
58
|
+
expect(first.line).toBeGreaterThan(0);
|
|
59
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return empty findings for clean file", async () => {
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(testDir, "tsconfig.json"),
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
compilerOptions: { strict: true, noEmit: true },
|
|
67
|
+
include: ["*.ts"],
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
Bun.spawnSync(["bun", "add", "typescript"], { cwd: testDir });
|
|
71
|
+
|
|
72
|
+
writeFileSync(join(testDir, "good.ts"), "const x: number = 42;\n");
|
|
73
|
+
|
|
74
|
+
const { runTypecheck } = await import("../typecheck");
|
|
75
|
+
const result = await runTypecheck(["good.ts"], testDir);
|
|
76
|
+
|
|
77
|
+
expect(result.findings).toEqual([]);
|
|
78
|
+
expect(result.tool).toBe("tsc");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should skip with info when tsc is not found", async () => {
|
|
82
|
+
// Use a non-existent tool path
|
|
83
|
+
const { runTypecheck } = await import("../typecheck");
|
|
84
|
+
const result = await runTypecheck(["file.ts"], testDir, {
|
|
85
|
+
command: "nonexistent-tsc-binary",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.findings).toEqual([]);
|
|
89
|
+
expect(result.skipped).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should detect language and use appropriate checker", async () => {
|
|
93
|
+
const { getTypecheckCommand } = await import("../typecheck");
|
|
94
|
+
|
|
95
|
+
expect(getTypecheckCommand("typescript")).toEqual(
|
|
96
|
+
expect.objectContaining({ tool: "tsc" }),
|
|
97
|
+
);
|
|
98
|
+
expect(getTypecheckCommand("python")).toEqual(
|
|
99
|
+
expect.objectContaining({ tool: "mypy" }),
|
|
100
|
+
);
|
|
101
|
+
expect(getTypecheckCommand("go")).toEqual(
|
|
102
|
+
expect.objectContaining({ tool: "go-vet" }),
|
|
103
|
+
);
|
|
104
|
+
expect(getTypecheckCommand("rust")).toEqual(
|
|
105
|
+
expect.objectContaining({ tool: "cargo-check" }),
|
|
106
|
+
);
|
|
107
|
+
expect(getTypecheckCommand("csharp")).toEqual(
|
|
108
|
+
expect.objectContaining({ tool: "dotnet-build" }),
|
|
109
|
+
);
|
|
110
|
+
expect(getTypecheckCommand("java")).toEqual(
|
|
111
|
+
expect.objectContaining({ tool: "javac" }),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should parse multiple errors from tsc output", async () => {
|
|
116
|
+
const { parseTscOutput } = await import("../typecheck");
|
|
117
|
+
|
|
118
|
+
const tscOutput = [
|
|
119
|
+
"src/foo.ts(10,5): error TS2322: Type 'string' is not assignable to type 'number'.",
|
|
120
|
+
"src/bar.ts(3,1): error TS2304: Cannot find name 'x'.",
|
|
121
|
+
].join("\n");
|
|
122
|
+
|
|
123
|
+
const findings = parseTscOutput(tscOutput);
|
|
124
|
+
|
|
125
|
+
expect(findings).toHaveLength(2);
|
|
126
|
+
expect(findings[0]).toEqual({
|
|
127
|
+
tool: "tsc",
|
|
128
|
+
file: "src/foo.ts",
|
|
129
|
+
line: 10,
|
|
130
|
+
column: 5,
|
|
131
|
+
message: "TS2322: Type 'string' is not assignable to type 'number'.",
|
|
132
|
+
severity: "error",
|
|
133
|
+
ruleId: "TS2322",
|
|
134
|
+
});
|
|
135
|
+
expect(findings[1]).toEqual({
|
|
136
|
+
tool: "tsc",
|
|
137
|
+
file: "src/bar.ts",
|
|
138
|
+
line: 3,
|
|
139
|
+
column: 1,
|
|
140
|
+
message: "TS2304: Cannot find name 'x'.",
|
|
141
|
+
severity: "error",
|
|
142
|
+
ruleId: "TS2304",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should return empty for empty tsc output", async () => {
|
|
147
|
+
const { parseTscOutput } = await import("../typecheck");
|
|
148
|
+
const findings = parseTscOutput("");
|
|
149
|
+
expect(findings).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should handle no tsconfig.json gracefully", async () => {
|
|
153
|
+
// No tsconfig.json in testDir
|
|
154
|
+
const { runTypecheck } = await import("../typecheck");
|
|
155
|
+
const result = await runTypecheck(["file.ts"], testDir);
|
|
156
|
+
|
|
157
|
+
// Should not crash, should skip or return empty
|
|
158
|
+
expect(result.skipped).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseZapJson, runZap } from "../zap";
|
|
3
|
+
|
|
4
|
+
// ─── parseZapJson ─────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe("parseZapJson", () => {
|
|
7
|
+
it("should return empty array for empty alerts", () => {
|
|
8
|
+
const json = JSON.stringify({ site: [{ alerts: [] }] });
|
|
9
|
+
const findings = parseZapJson(json);
|
|
10
|
+
expect(findings).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should parse a single alert from ZAP JSON output", () => {
|
|
14
|
+
const json = JSON.stringify({
|
|
15
|
+
site: [
|
|
16
|
+
{
|
|
17
|
+
alerts: [
|
|
18
|
+
{
|
|
19
|
+
pluginid: "10021",
|
|
20
|
+
alert: "X-Content-Type-Options Header Missing",
|
|
21
|
+
riskdesc: "Low (Medium)",
|
|
22
|
+
desc: "The Anti-MIME-Sniffing header is not set.",
|
|
23
|
+
instances: [
|
|
24
|
+
{
|
|
25
|
+
uri: "https://example.com/api/health",
|
|
26
|
+
method: "GET",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const findings = parseZapJson(json);
|
|
36
|
+
expect(findings.length).toBe(1);
|
|
37
|
+
expect(findings[0]?.tool).toBe("zap");
|
|
38
|
+
expect(findings[0]?.file).toBe("https://example.com/api/health");
|
|
39
|
+
expect(findings[0]?.line).toBe(0);
|
|
40
|
+
expect(findings[0]?.message).toContain(
|
|
41
|
+
"X-Content-Type-Options Header Missing",
|
|
42
|
+
);
|
|
43
|
+
expect(findings[0]?.severity).toBe("info");
|
|
44
|
+
expect(findings[0]?.ruleId).toBe("10021");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should map ZAP risk levels to severity correctly", () => {
|
|
48
|
+
const makeZap = (riskdesc: string) =>
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
site: [
|
|
51
|
+
{
|
|
52
|
+
alerts: [
|
|
53
|
+
{
|
|
54
|
+
pluginid: "10001",
|
|
55
|
+
alert: "Test Alert",
|
|
56
|
+
riskdesc,
|
|
57
|
+
desc: "Test description",
|
|
58
|
+
instances: [{ uri: "https://example.com", method: "GET" }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(parseZapJson(makeZap("High (Medium)"))[0]?.severity).toBe("error");
|
|
66
|
+
expect(parseZapJson(makeZap("Medium (Low)"))[0]?.severity).toBe("warning");
|
|
67
|
+
expect(parseZapJson(makeZap("Low (Medium)"))[0]?.severity).toBe("info");
|
|
68
|
+
expect(parseZapJson(makeZap("Informational (Low)"))[0]?.severity).toBe(
|
|
69
|
+
"info",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle multiple alerts with multiple instances", () => {
|
|
74
|
+
const json = JSON.stringify({
|
|
75
|
+
site: [
|
|
76
|
+
{
|
|
77
|
+
alerts: [
|
|
78
|
+
{
|
|
79
|
+
pluginid: "10021",
|
|
80
|
+
alert: "Alert A",
|
|
81
|
+
riskdesc: "High (Medium)",
|
|
82
|
+
desc: "Description A",
|
|
83
|
+
instances: [
|
|
84
|
+
{ uri: "https://example.com/a", method: "GET" },
|
|
85
|
+
{ uri: "https://example.com/b", method: "POST" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
pluginid: "10022",
|
|
90
|
+
alert: "Alert B",
|
|
91
|
+
riskdesc: "Low (Low)",
|
|
92
|
+
desc: "Description B",
|
|
93
|
+
instances: [{ uri: "https://example.com/c", method: "GET" }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const findings = parseZapJson(json);
|
|
101
|
+
expect(findings.length).toBe(3);
|
|
102
|
+
expect(findings[0]?.file).toBe("https://example.com/a");
|
|
103
|
+
expect(findings[1]?.file).toBe("https://example.com/b");
|
|
104
|
+
expect(findings[2]?.file).toBe("https://example.com/c");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle alerts with no instances", () => {
|
|
108
|
+
const json = JSON.stringify({
|
|
109
|
+
site: [
|
|
110
|
+
{
|
|
111
|
+
alerts: [
|
|
112
|
+
{
|
|
113
|
+
pluginid: "10021",
|
|
114
|
+
alert: "No Instances Alert",
|
|
115
|
+
riskdesc: "Medium (Medium)",
|
|
116
|
+
desc: "No instances",
|
|
117
|
+
instances: [],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const findings = parseZapJson(json);
|
|
125
|
+
expect(findings.length).toBe(1);
|
|
126
|
+
expect(findings[0]?.file).toBe("");
|
|
127
|
+
expect(findings[0]?.message).toContain("No Instances Alert");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should return empty array for invalid JSON", () => {
|
|
131
|
+
const findings = parseZapJson("not valid json {{{");
|
|
132
|
+
expect(findings).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should return empty array for malformed structure", () => {
|
|
136
|
+
const findings = parseZapJson(JSON.stringify({ unexpected: true }));
|
|
137
|
+
expect(findings).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle missing site array gracefully", () => {
|
|
141
|
+
const findings = parseZapJson(JSON.stringify({ site: "not-an-array" }));
|
|
142
|
+
expect(findings).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should handle site entries without alerts", () => {
|
|
146
|
+
const json = JSON.stringify({
|
|
147
|
+
site: [{ name: "example.com" }],
|
|
148
|
+
});
|
|
149
|
+
const findings = parseZapJson(json);
|
|
150
|
+
expect(findings).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── runZap ───────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe("runZap", () => {
|
|
157
|
+
it("should skip when docker is not available", async () => {
|
|
158
|
+
const result = await runZap({
|
|
159
|
+
targetUrl: "https://example.com",
|
|
160
|
+
cwd: "/tmp",
|
|
161
|
+
available: false,
|
|
162
|
+
});
|
|
163
|
+
expect(result.findings).toEqual([]);
|
|
164
|
+
expect(result.skipped).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should skip when no targetUrl is provided", async () => {
|
|
168
|
+
const result = await runZap({
|
|
169
|
+
targetUrl: "",
|
|
170
|
+
cwd: "/tmp",
|
|
171
|
+
available: true,
|
|
172
|
+
});
|
|
173
|
+
expect(result.findings).toEqual([]);
|
|
174
|
+
expect(result.skipped).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should return correct result shape", async () => {
|
|
178
|
+
const result = await runZap({
|
|
179
|
+
targetUrl: "https://example.com",
|
|
180
|
+
cwd: "/tmp",
|
|
181
|
+
available: false,
|
|
182
|
+
});
|
|
183
|
+
expect(result).toHaveProperty("findings");
|
|
184
|
+
expect(result).toHaveProperty("skipped");
|
|
185
|
+
expect(Array.isArray(result.findings)).toBe(true);
|
|
186
|
+
expect(typeof result.skipped).toBe("boolean");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-function Consistency Check — deterministic AST-based analysis.
|
|
3
|
+
*
|
|
4
|
+
* Catches the class of bug that lost Maina 2 points in the Tier 3 benchmark:
|
|
5
|
+
* functions that call a validator on one code path but skip it on another.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* 1. Spec-based: reads spec.md / constitution.md for stated constraints,
|
|
9
|
+
* builds a rule set, checks compliance
|
|
10
|
+
* 2. Heuristic: if no spec exists, looks for inconsistent validator usage
|
|
11
|
+
* patterns across related functions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import type { Finding } from "./diff-filter";
|
|
18
|
+
|
|
19
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface ConsistencyRule {
|
|
22
|
+
pattern: string;
|
|
23
|
+
source: "spec" | "heuristic";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ConsistencyResult {
|
|
27
|
+
findings: Finding[];
|
|
28
|
+
rulesChecked: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Rule Extraction ─────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract consistency rules from spec/constitution content.
|
|
35
|
+
* Looks for patterns like "use X when Y", "always call X", "validate with X".
|
|
36
|
+
*/
|
|
37
|
+
function extractRulesFromSpec(content: string): ConsistencyRule[] {
|
|
38
|
+
const rules: ConsistencyRule[] = [];
|
|
39
|
+
const patterns = [
|
|
40
|
+
/always (?:use|call|check|validate with) (\w+)/gi,
|
|
41
|
+
/must (?:use|call|check) (\w+)/gi,
|
|
42
|
+
/validate.*(?:with|using) (\w+)/gi,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const pattern of patterns) {
|
|
46
|
+
for (const match of content.matchAll(pattern)) {
|
|
47
|
+
rules.push({
|
|
48
|
+
pattern: match[1] ?? "",
|
|
49
|
+
source: "spec",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return rules;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load spec/constitution content from the maina directory.
|
|
59
|
+
*/
|
|
60
|
+
function loadSpecContent(mainaDir: string): string {
|
|
61
|
+
const paths = [join(mainaDir, "constitution.md"), join(mainaDir, "spec.md")];
|
|
62
|
+
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
for (const p of paths) {
|
|
65
|
+
if (existsSync(p)) {
|
|
66
|
+
parts.push(readFileSync(p, "utf-8"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Heuristic Analysis ──────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find functions that call validators inconsistently.
|
|
77
|
+
* If functionA calls isValid(x) and functionB doesn't but both take similar
|
|
78
|
+
* params, that's a potential inconsistency.
|
|
79
|
+
*/
|
|
80
|
+
function findHeuristicIssues(source: string, file: string): Finding[] {
|
|
81
|
+
const findings: Finding[] = [];
|
|
82
|
+
|
|
83
|
+
// Extract function calls per function body
|
|
84
|
+
const functionPattern =
|
|
85
|
+
/function\s+(\w+)\s*\([^)]*\)\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
86
|
+
const functions: Array<{ name: string; body: string; line: number }> = [];
|
|
87
|
+
|
|
88
|
+
for (const match of source.matchAll(functionPattern)) {
|
|
89
|
+
const lineNumber = source.substring(0, match.index ?? 0).split("\n").length;
|
|
90
|
+
functions.push({
|
|
91
|
+
name: match[1] ?? "",
|
|
92
|
+
body: match[2] ?? "",
|
|
93
|
+
line: lineNumber,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find validator-like calls (isX, validateX, checkX)
|
|
98
|
+
const validatorPattern = /\b(is[A-Z]\w+|validate\w+|check\w+)\s*\(/g;
|
|
99
|
+
const validatorsByFunction = new Map<string, Set<string>>();
|
|
100
|
+
|
|
101
|
+
for (const fn of functions) {
|
|
102
|
+
const validators = new Set<string>();
|
|
103
|
+
for (const validatorMatch of fn.body.matchAll(validatorPattern)) {
|
|
104
|
+
validators.add(validatorMatch[1] ?? "");
|
|
105
|
+
}
|
|
106
|
+
validatorsByFunction.set(fn.name, validators);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Compare: if most functions use a validator but one doesn't, flag it
|
|
110
|
+
const allValidators = new Set<string>();
|
|
111
|
+
for (const validators of validatorsByFunction.values()) {
|
|
112
|
+
for (const v of validators) allValidators.add(v);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (functions.length >= 2) {
|
|
116
|
+
for (const validator of allValidators) {
|
|
117
|
+
const usersCount = Array.from(validatorsByFunction.values()).filter((v) =>
|
|
118
|
+
v.has(validator),
|
|
119
|
+
).length;
|
|
120
|
+
|
|
121
|
+
// If majority uses it but some don't, flag the ones that don't
|
|
122
|
+
if (usersCount > 0 && usersCount < functions.length) {
|
|
123
|
+
for (const fn of functions) {
|
|
124
|
+
const fnValidators = validatorsByFunction.get(fn.name);
|
|
125
|
+
if (fnValidators && !fnValidators.has(validator)) {
|
|
126
|
+
findings.push({
|
|
127
|
+
tool: "consistency",
|
|
128
|
+
file,
|
|
129
|
+
line: fn.line,
|
|
130
|
+
message: `Function '${fn.name}' does not call '${validator}' — other functions in this file do. Possible inconsistency.`,
|
|
131
|
+
severity: "warning",
|
|
132
|
+
ruleId: `consistency/${validator}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return findings;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Main ────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export async function checkConsistency(
|
|
146
|
+
files: string[],
|
|
147
|
+
cwd: string,
|
|
148
|
+
mainaDir: string,
|
|
149
|
+
): Promise<ConsistencyResult> {
|
|
150
|
+
const specContent = existsSync(mainaDir) ? loadSpecContent(mainaDir) : "";
|
|
151
|
+
const specRules = extractRulesFromSpec(specContent);
|
|
152
|
+
const allFindings: Finding[] = [];
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const filePath = join(cwd, file);
|
|
156
|
+
if (!existsSync(filePath)) continue;
|
|
157
|
+
|
|
158
|
+
const source = readFileSync(filePath, "utf-8");
|
|
159
|
+
|
|
160
|
+
// Check spec-based rules
|
|
161
|
+
for (const rule of specRules) {
|
|
162
|
+
const callPattern = new RegExp(`\\b${rule.pattern}\\s*\\(`, "g");
|
|
163
|
+
const fnPattern = /function\s+(\w+)/g;
|
|
164
|
+
|
|
165
|
+
// Find functions that should use this pattern but don't
|
|
166
|
+
for (const fnMatch of source.matchAll(fnPattern)) {
|
|
167
|
+
const fnStart = fnMatch.index ?? 0;
|
|
168
|
+
const fnEnd = source.indexOf("}", fnStart + 1);
|
|
169
|
+
if (fnEnd === -1) continue;
|
|
170
|
+
|
|
171
|
+
const fnBody = source.substring(fnStart, fnEnd);
|
|
172
|
+
if (!callPattern.test(fnBody)) {
|
|
173
|
+
const relatedTerms = rule.pattern
|
|
174
|
+
.toLowerCase()
|
|
175
|
+
.replace(/^is|^validate|^check/, "");
|
|
176
|
+
if (fnBody.toLowerCase().includes(relatedTerms)) {
|
|
177
|
+
const line = source.substring(0, fnStart).split("\n").length;
|
|
178
|
+
allFindings.push({
|
|
179
|
+
tool: "consistency",
|
|
180
|
+
file,
|
|
181
|
+
line,
|
|
182
|
+
message: `Spec requires '${rule.pattern}' — function '${fnMatch[1]}' may need it.`,
|
|
183
|
+
severity: "warning",
|
|
184
|
+
ruleId: `consistency/spec-${rule.pattern}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Heuristic checks (always run)
|
|
192
|
+
allFindings.push(...findHeuristicIssues(source, file));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
findings: allFindings,
|
|
197
|
+
rulesChecked: specRules.length + 1, // +1 for heuristic check
|
|
198
|
+
};
|
|
199
|
+
}
|
package/src/verify/detect.ts
CHANGED
|
@@ -26,7 +26,9 @@ export type ToolName =
|
|
|
26
26
|
| "dotnet-format"
|
|
27
27
|
| "checkstyle"
|
|
28
28
|
| "spotbugs"
|
|
29
|
-
| "pmd"
|
|
29
|
+
| "pmd"
|
|
30
|
+
| "zap"
|
|
31
|
+
| "lighthouse";
|
|
30
32
|
|
|
31
33
|
export interface DetectedTool {
|
|
32
34
|
name: string;
|
|
@@ -55,6 +57,8 @@ export const TOOL_REGISTRY: Record<
|
|
|
55
57
|
checkstyle: { command: "checkstyle", versionFlag: "--version" },
|
|
56
58
|
spotbugs: { command: "spotbugs", versionFlag: "-version" },
|
|
57
59
|
pmd: { command: "pmd", versionFlag: "--version" },
|
|
60
|
+
zap: { command: "docker", versionFlag: "--version" },
|
|
61
|
+
lighthouse: { command: "lighthouse", versionFlag: "--version" },
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
/**
|