@pablozrrrr/jevguard 0.1.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/LICENSE +21 -0
- package/README.md +417 -0
- package/cli/main.js +1813 -0
- package/index.js +2022 -0
- package/package.json +53 -0
- package/skills/jevguard-rules/SKILL.md +158 -0
- package/skills/jevguard-rules/validate-rules.js +991 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/plugin/src/skills/jevguard-rules/validate-rules.ts
|
|
3
|
+
import { readFileSync, statSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/core/src/ports/types.ts
|
|
7
|
+
var SCOPE_CREEP_ANSWER = "scopeCreep";
|
|
8
|
+
var COMPLEXITY_ANSWER = "complexity";
|
|
9
|
+
// packages/core/src/evaluation/build-request.ts
|
|
10
|
+
var NOUL_INSTRUCTIONS = "Answer true when the attributed change violates the rule's Violation, excluding behavior described by the rule's Allowed exception. Answer false when the change does not violate the rule, including allowed behavior. Return the probability from 0 to 1 that the change violates the rule.";
|
|
11
|
+
function buildJevRequest(task, rule, evidence) {
|
|
12
|
+
return {
|
|
13
|
+
kind: "RULE",
|
|
14
|
+
task,
|
|
15
|
+
question: {
|
|
16
|
+
type: "noul",
|
|
17
|
+
instructions: NOUL_INSTRUCTIONS,
|
|
18
|
+
criteria: {
|
|
19
|
+
id: rule.id,
|
|
20
|
+
description: rule.description,
|
|
21
|
+
violation: rule.violation,
|
|
22
|
+
...rule.allowed === null ? {} : { allowed: rule.allowed }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// packages/core/src/evaluation/build-context.ts
|
|
29
|
+
function buildReviewContext(turn, rule, scopedPaths) {
|
|
30
|
+
return {
|
|
31
|
+
kind: "RULE",
|
|
32
|
+
turnId: turn.id,
|
|
33
|
+
ruleId: rule.id,
|
|
34
|
+
severity: rule.severity,
|
|
35
|
+
scopedPaths
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// packages/core/src/evidence/defaults.ts
|
|
39
|
+
var DEFAULT_MAX_DIFF_LENGTH = 1e5;
|
|
40
|
+
var DEFAULT_EVIDENCE_POLICY = {
|
|
41
|
+
maxDiffLength: DEFAULT_MAX_DIFF_LENGTH,
|
|
42
|
+
allowedExtensions: [
|
|
43
|
+
".ts",
|
|
44
|
+
".tsx",
|
|
45
|
+
".mts",
|
|
46
|
+
".cts",
|
|
47
|
+
".js",
|
|
48
|
+
".jsx",
|
|
49
|
+
".mjs",
|
|
50
|
+
".cjs",
|
|
51
|
+
".json",
|
|
52
|
+
".jsonc",
|
|
53
|
+
".md",
|
|
54
|
+
".mdx",
|
|
55
|
+
".txt",
|
|
56
|
+
".yaml",
|
|
57
|
+
".yml",
|
|
58
|
+
".toml",
|
|
59
|
+
".css",
|
|
60
|
+
".scss",
|
|
61
|
+
".sass",
|
|
62
|
+
".less",
|
|
63
|
+
".html",
|
|
64
|
+
".htm",
|
|
65
|
+
".vue",
|
|
66
|
+
".svelte",
|
|
67
|
+
".py",
|
|
68
|
+
".rb",
|
|
69
|
+
".go",
|
|
70
|
+
".rs",
|
|
71
|
+
".java",
|
|
72
|
+
".kt",
|
|
73
|
+
".kts",
|
|
74
|
+
".scala",
|
|
75
|
+
".cs",
|
|
76
|
+
".php",
|
|
77
|
+
".c",
|
|
78
|
+
".h",
|
|
79
|
+
".cc",
|
|
80
|
+
".cpp",
|
|
81
|
+
".hpp",
|
|
82
|
+
".cxx",
|
|
83
|
+
".hh",
|
|
84
|
+
".swift",
|
|
85
|
+
".dart",
|
|
86
|
+
".lua",
|
|
87
|
+
".r",
|
|
88
|
+
".ex",
|
|
89
|
+
".exs",
|
|
90
|
+
".erl",
|
|
91
|
+
".sh",
|
|
92
|
+
".bash",
|
|
93
|
+
".zsh",
|
|
94
|
+
".fish",
|
|
95
|
+
".ps1",
|
|
96
|
+
".bat",
|
|
97
|
+
".cmd",
|
|
98
|
+
".sql",
|
|
99
|
+
".graphql",
|
|
100
|
+
".gql",
|
|
101
|
+
".proto",
|
|
102
|
+
".tf",
|
|
103
|
+
".hcl",
|
|
104
|
+
".xml",
|
|
105
|
+
".svg",
|
|
106
|
+
".csv",
|
|
107
|
+
".tsv",
|
|
108
|
+
".diff",
|
|
109
|
+
".patch"
|
|
110
|
+
],
|
|
111
|
+
deniedFileNames: [
|
|
112
|
+
".env",
|
|
113
|
+
".envrc",
|
|
114
|
+
".netrc",
|
|
115
|
+
"_netrc",
|
|
116
|
+
".npmrc",
|
|
117
|
+
".pypirc",
|
|
118
|
+
".pgpass",
|
|
119
|
+
".git-credentials",
|
|
120
|
+
".htpasswd",
|
|
121
|
+
".dockercfg",
|
|
122
|
+
"id_rsa",
|
|
123
|
+
"id_dsa",
|
|
124
|
+
"id_ecdsa",
|
|
125
|
+
"id_ed25519",
|
|
126
|
+
"credentials",
|
|
127
|
+
"credentials.json",
|
|
128
|
+
"credentials.yaml",
|
|
129
|
+
"credentials.yml",
|
|
130
|
+
"secrets",
|
|
131
|
+
"secrets.json",
|
|
132
|
+
"secrets.yaml",
|
|
133
|
+
"secrets.yml",
|
|
134
|
+
"shadow",
|
|
135
|
+
"master.key"
|
|
136
|
+
],
|
|
137
|
+
deniedExtensions: [
|
|
138
|
+
".pem",
|
|
139
|
+
".key",
|
|
140
|
+
".p12",
|
|
141
|
+
".pfx",
|
|
142
|
+
".pkcs12",
|
|
143
|
+
".jks",
|
|
144
|
+
".keystore",
|
|
145
|
+
".ppk",
|
|
146
|
+
".p8",
|
|
147
|
+
".der"
|
|
148
|
+
],
|
|
149
|
+
deniedDirectoryNames: [".ssh", ".aws", ".gnupg", ".gpg", ".azure", ".kube", ".docker"]
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// packages/core/src/evidence/paths.ts
|
|
153
|
+
function normalizePath(path) {
|
|
154
|
+
return path.replace(/\\/g, "/");
|
|
155
|
+
}
|
|
156
|
+
function pathSegments(path) {
|
|
157
|
+
return normalizePath(path).split("/").filter((segment) => segment !== "");
|
|
158
|
+
}
|
|
159
|
+
function pathBasename(path) {
|
|
160
|
+
const segments = pathSegments(path);
|
|
161
|
+
return segments[segments.length - 1] ?? "";
|
|
162
|
+
}
|
|
163
|
+
function fileExtension(path) {
|
|
164
|
+
const basename = pathBasename(path).toLowerCase();
|
|
165
|
+
const dot = basename.lastIndexOf(".");
|
|
166
|
+
return dot <= 0 ? "" : basename.slice(dot);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// packages/core/src/evidence/safety.ts
|
|
170
|
+
var DENIED_SENSITIVE = { allowed: false, reason: "DENIED_SENSITIVE_PATH" };
|
|
171
|
+
var DENIED_EXTENSION = { allowed: false, reason: "EXTENSION_NOT_ALLOWED" };
|
|
172
|
+
function checkFilePath(path, policy) {
|
|
173
|
+
const basename = pathBasename(path).toLowerCase();
|
|
174
|
+
if (matchesDeniedFileName(basename, policy)) {
|
|
175
|
+
return DENIED_SENSITIVE;
|
|
176
|
+
}
|
|
177
|
+
const segments = pathSegments(path).map((segment) => segment.toLowerCase());
|
|
178
|
+
if (segments.some((segment) => policy.deniedDirectoryNames.includes(segment))) {
|
|
179
|
+
return DENIED_SENSITIVE;
|
|
180
|
+
}
|
|
181
|
+
const extension = fileExtension(path);
|
|
182
|
+
if (policy.deniedExtensions.includes(extension)) {
|
|
183
|
+
return DENIED_SENSITIVE;
|
|
184
|
+
}
|
|
185
|
+
if (!policy.allowedExtensions.includes(extension)) {
|
|
186
|
+
return DENIED_EXTENSION;
|
|
187
|
+
}
|
|
188
|
+
return { allowed: true };
|
|
189
|
+
}
|
|
190
|
+
function matchesDeniedFileName(basename, policy) {
|
|
191
|
+
return policy.deniedFileNames.some((name) => name.startsWith(".") ? basename === name || basename.startsWith(`${name}.`) : basename === name);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// packages/core/src/evidence/scope.ts
|
|
195
|
+
function matchesScope(path, scope) {
|
|
196
|
+
return globToRegExp(scope).test(normalizePath(path));
|
|
197
|
+
}
|
|
198
|
+
function globToRegExp(glob) {
|
|
199
|
+
const source = glob.replace(/\\/g, "/");
|
|
200
|
+
let pattern = "";
|
|
201
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
202
|
+
const char = source[index] ?? "";
|
|
203
|
+
if (char === "*") {
|
|
204
|
+
if (source[index + 1] === "*") {
|
|
205
|
+
const followedBySlash = source[index + 2] === "/";
|
|
206
|
+
pattern += followedBySlash ? "(?:.*/)?" : ".*";
|
|
207
|
+
index += followedBySlash ? 2 : 1;
|
|
208
|
+
} else {
|
|
209
|
+
pattern += "[^/]*";
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "?") {
|
|
214
|
+
pattern += "[^/]";
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
pattern += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
218
|
+
}
|
|
219
|
+
return new RegExp(`^${pattern}$`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// packages/core/src/evidence/select-evidence.ts
|
|
223
|
+
function selectRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
224
|
+
if (hasNoAttributedPatch(turn)) {
|
|
225
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
226
|
+
}
|
|
227
|
+
const applicableFiles = selectApplicableFiles(turn.files, rule.scope);
|
|
228
|
+
if (applicableFiles.length === 0) {
|
|
229
|
+
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
230
|
+
}
|
|
231
|
+
return mountEvidence(applicableFiles, policy);
|
|
232
|
+
}
|
|
233
|
+
function selectTurnEvidence(turn, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
234
|
+
if (hasNoAttributedPatch(turn)) {
|
|
235
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
236
|
+
}
|
|
237
|
+
const attributedFiles = turn.files.filter((file) => file.patch.trim() !== "");
|
|
238
|
+
return mountEvidence(attributedFiles, policy);
|
|
239
|
+
}
|
|
240
|
+
function mountEvidence(files, policy) {
|
|
241
|
+
const diff = assembleDiff(files);
|
|
242
|
+
if (diff.trim() === "") {
|
|
243
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
244
|
+
}
|
|
245
|
+
if (diff.length > policy.maxDiffLength) {
|
|
246
|
+
return { status: "UNAVAILABLE", reason: "OVERSIZED_DIFF" };
|
|
247
|
+
}
|
|
248
|
+
if (files.some((file) => !checkFilePath(file.path, policy).allowed)) {
|
|
249
|
+
return { status: "UNAVAILABLE", reason: "BLOCKED_EVIDENCE" };
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
status: "SELECTED",
|
|
253
|
+
evidence: { files: files.map((file) => file.path), diff }
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function hasNoAttributedPatch(turn) {
|
|
257
|
+
return turn.files.every((file) => file.patch.trim() === "");
|
|
258
|
+
}
|
|
259
|
+
function selectApplicableFiles(files, scope) {
|
|
260
|
+
if (scope === null) {
|
|
261
|
+
return files;
|
|
262
|
+
}
|
|
263
|
+
return files.filter((file) => matchesScope(file.path, scope));
|
|
264
|
+
}
|
|
265
|
+
function assembleDiff(files) {
|
|
266
|
+
return files.map((file) => file.patch).join(`
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// packages/core/src/gate/probability.ts
|
|
271
|
+
function isProbability(value) {
|
|
272
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// packages/core/src/gate/evaluate-gate.ts
|
|
276
|
+
function evaluateGate(severity, violationProbability, config) {
|
|
277
|
+
if (!isProbability(violationProbability)) {
|
|
278
|
+
return { outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
279
|
+
}
|
|
280
|
+
switch (severity) {
|
|
281
|
+
case "error":
|
|
282
|
+
return evaluateErrorGate(violationProbability, config.thresholds.error);
|
|
283
|
+
case "warning":
|
|
284
|
+
return evaluateWarningGate(violationProbability, config.thresholds.warning);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function evaluateErrorGate(probability, thresholds) {
|
|
288
|
+
if (probability < thresholds.warn) {
|
|
289
|
+
return { outcome: "PASS", violationProbability: probability };
|
|
290
|
+
}
|
|
291
|
+
if (probability < thresholds.fail) {
|
|
292
|
+
return { outcome: "WARN", violationProbability: probability };
|
|
293
|
+
}
|
|
294
|
+
return { outcome: "FAIL", violationProbability: probability };
|
|
295
|
+
}
|
|
296
|
+
function evaluateWarningGate(probability, thresholds) {
|
|
297
|
+
if (probability < thresholds.warn) {
|
|
298
|
+
return { outcome: "PASS", violationProbability: probability };
|
|
299
|
+
}
|
|
300
|
+
return { outcome: "WARN", violationProbability: probability };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// packages/core/src/evaluation/evaluate-with-port.ts
|
|
304
|
+
async function evaluateWithPort(port, request) {
|
|
305
|
+
try {
|
|
306
|
+
const result = await port.evaluate(request);
|
|
307
|
+
return result.kind === "RULE" ? result : { kind: "RULE", status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
308
|
+
} catch {
|
|
309
|
+
return { kind: "RULE", status: "FAILED", reason: "API_ERROR" };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function evaluateBuiltInBatchWithPort(port, request) {
|
|
313
|
+
try {
|
|
314
|
+
const result = await port.evaluate(request);
|
|
315
|
+
return result.kind === "BUILT_IN_BATCH" ? result : { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
316
|
+
} catch {
|
|
317
|
+
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "API_ERROR" };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// packages/core/src/evaluation/evaluate-rule.ts
|
|
322
|
+
async function evaluateRule(input, dependencies) {
|
|
323
|
+
const selection = selectRuleEvidence(input.turn, input.rule, input.evidencePolicy);
|
|
324
|
+
if (selection.status === "SKIPPED") {
|
|
325
|
+
const context = buildReviewContext(input.turn, input.rule, []);
|
|
326
|
+
return { ...context, outcome: "SKIPPED", reason: selection.reason };
|
|
327
|
+
}
|
|
328
|
+
if (selection.status === "UNAVAILABLE") {
|
|
329
|
+
const context = buildReviewContext(input.turn, input.rule, []);
|
|
330
|
+
return { ...context, outcome: "UNAVAILABLE", reason: selection.reason };
|
|
331
|
+
}
|
|
332
|
+
const context = buildReviewContext(input.turn, input.rule, selection.evidence.files);
|
|
333
|
+
const request = buildJevRequest(input.turn.task, input.rule, selection.evidence);
|
|
334
|
+
const evaluation = await evaluateWithPort(dependencies.jev, request);
|
|
335
|
+
if (evaluation.status === "FAILED") {
|
|
336
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
337
|
+
}
|
|
338
|
+
const gate = evaluateGate(input.rule.severity, evaluation.noul.violationProbability, input.gateConfig);
|
|
339
|
+
return { ...context, ...gate };
|
|
340
|
+
}
|
|
341
|
+
// packages/core/src/review/aggregate.ts
|
|
342
|
+
var VERDICT_RANK = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
343
|
+
function aggregateReview(turnId, results) {
|
|
344
|
+
const snapshot = [...results];
|
|
345
|
+
return { turnId, results: snapshot, summary: summarize(snapshot) };
|
|
346
|
+
}
|
|
347
|
+
function summarize(results) {
|
|
348
|
+
const counts = countOutcomes(results);
|
|
349
|
+
return {
|
|
350
|
+
verdict: highestVerdict(results),
|
|
351
|
+
hasUnavailable: counts.unavailable > 0,
|
|
352
|
+
counts
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function countOutcomes(results) {
|
|
356
|
+
const counts = { pass: 0, warn: 0, fail: 0, skipped: 0, unavailable: 0 };
|
|
357
|
+
for (const result of results) {
|
|
358
|
+
switch (result.outcome) {
|
|
359
|
+
case "PASS":
|
|
360
|
+
counts.pass += 1;
|
|
361
|
+
break;
|
|
362
|
+
case "WARN":
|
|
363
|
+
counts.warn += 1;
|
|
364
|
+
break;
|
|
365
|
+
case "FAIL":
|
|
366
|
+
counts.fail += 1;
|
|
367
|
+
break;
|
|
368
|
+
case "SKIPPED":
|
|
369
|
+
counts.skipped += 1;
|
|
370
|
+
break;
|
|
371
|
+
case "UNAVAILABLE":
|
|
372
|
+
counts.unavailable += 1;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return counts;
|
|
377
|
+
}
|
|
378
|
+
function highestVerdict(results) {
|
|
379
|
+
let highest = null;
|
|
380
|
+
for (const result of results) {
|
|
381
|
+
if (!isSemanticVerdict(result.outcome)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (highest === null || VERDICT_RANK[result.outcome] > VERDICT_RANK[highest]) {
|
|
385
|
+
highest = result.outcome;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return highest;
|
|
389
|
+
}
|
|
390
|
+
function isSemanticVerdict(outcome) {
|
|
391
|
+
return outcome === "PASS" || outcome === "WARN" || outcome === "FAIL";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// packages/core/src/evaluation/evaluate-rules.ts
|
|
395
|
+
async function evaluateRules(input, dependencies) {
|
|
396
|
+
const results = [];
|
|
397
|
+
for (const rule of input.rules) {
|
|
398
|
+
results.push(await evaluateRuleResult(input, rule, dependencies));
|
|
399
|
+
}
|
|
400
|
+
return aggregateReview(input.turn.id, results);
|
|
401
|
+
}
|
|
402
|
+
async function evaluateRuleResult(input, rule, dependencies) {
|
|
403
|
+
if (rule.status === "INVALID") {
|
|
404
|
+
return invalidRuleResult(input.turn.id, rule);
|
|
405
|
+
}
|
|
406
|
+
if (input.gateConfig.status === "INVALID") {
|
|
407
|
+
return invalidConfigResult(input.turn, rule.rule);
|
|
408
|
+
}
|
|
409
|
+
const ruleInput = {
|
|
410
|
+
turn: input.turn,
|
|
411
|
+
rule: rule.rule,
|
|
412
|
+
gateConfig: input.gateConfig.config,
|
|
413
|
+
evidencePolicy: input.evidencePolicy
|
|
414
|
+
};
|
|
415
|
+
return evaluateRule(ruleInput, dependencies);
|
|
416
|
+
}
|
|
417
|
+
function invalidRuleResult(turnId, failure) {
|
|
418
|
+
return {
|
|
419
|
+
kind: "RULE",
|
|
420
|
+
turnId,
|
|
421
|
+
ruleId: failure.ruleId,
|
|
422
|
+
severity: null,
|
|
423
|
+
scopedPaths: [],
|
|
424
|
+
outcome: "UNAVAILABLE",
|
|
425
|
+
reason: "INVALID_RULE"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function invalidConfigResult(turn, rule) {
|
|
429
|
+
return {
|
|
430
|
+
...buildReviewContext(turn, rule, []),
|
|
431
|
+
outcome: "UNAVAILABLE",
|
|
432
|
+
reason: "INVALID_CONFIG"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// packages/core/src/rules/headings.ts
|
|
436
|
+
var RULE_HEADING = /^##\s+(.*)$/;
|
|
437
|
+
var SECTION_HEADING = /^###\s+(.*)$/;
|
|
438
|
+
function matchRuleHeading(line) {
|
|
439
|
+
const match = RULE_HEADING.exec(line);
|
|
440
|
+
if (match === null) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return (match[1] ?? "").trim();
|
|
444
|
+
}
|
|
445
|
+
function matchSectionHeading(line) {
|
|
446
|
+
const match = SECTION_HEADING.exec(line);
|
|
447
|
+
if (match === null) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return (match[1] ?? "").trim();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// packages/core/src/rules/metadata.ts
|
|
454
|
+
var METADATA_ENTRY = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
455
|
+
function parseRuleMetadata(lines) {
|
|
456
|
+
let severity = null;
|
|
457
|
+
let scope = null;
|
|
458
|
+
const seenKeys = new Set;
|
|
459
|
+
for (const line of collectMetadataLines(lines)) {
|
|
460
|
+
const entry = parseMetadataEntry(line);
|
|
461
|
+
if (entry === null) {
|
|
462
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
463
|
+
}
|
|
464
|
+
if (seenKeys.has(entry.key)) {
|
|
465
|
+
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
466
|
+
}
|
|
467
|
+
seenKeys.add(entry.key);
|
|
468
|
+
if (entry.key === "severity") {
|
|
469
|
+
const parsed = parseSeverity(entry.value);
|
|
470
|
+
if (parsed === null) {
|
|
471
|
+
return { ok: false, code: "INVALID_SEVERITY" };
|
|
472
|
+
}
|
|
473
|
+
severity = parsed;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (entry.key === "scope") {
|
|
477
|
+
if (entry.value === "") {
|
|
478
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
479
|
+
}
|
|
480
|
+
scope = entry.value;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
484
|
+
}
|
|
485
|
+
if (severity === null) {
|
|
486
|
+
return { ok: false, code: "MISSING_SEVERITY" };
|
|
487
|
+
}
|
|
488
|
+
return { ok: true, value: { severity, scope } };
|
|
489
|
+
}
|
|
490
|
+
function collectMetadataLines(lines) {
|
|
491
|
+
const collected = [];
|
|
492
|
+
for (const line of lines) {
|
|
493
|
+
if (matchSectionHeading(line) !== null) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
if (line.trim() !== "") {
|
|
497
|
+
collected.push(line);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return collected;
|
|
501
|
+
}
|
|
502
|
+
function parseMetadataEntry(line) {
|
|
503
|
+
const match = METADATA_ENTRY.exec(line);
|
|
504
|
+
if (match === null) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
return { key: match[1] ?? "", value: (match[2] ?? "").trim() };
|
|
508
|
+
}
|
|
509
|
+
function parseSeverity(value) {
|
|
510
|
+
switch (value) {
|
|
511
|
+
case "error":
|
|
512
|
+
case "warning":
|
|
513
|
+
return value;
|
|
514
|
+
default:
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// packages/core/src/rules/sections.ts
|
|
520
|
+
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
521
|
+
function parseRuleSections(lines) {
|
|
522
|
+
const collected = collectSections(lines);
|
|
523
|
+
if (!collected.ok) {
|
|
524
|
+
return collected;
|
|
525
|
+
}
|
|
526
|
+
return validateSections(collected.value);
|
|
527
|
+
}
|
|
528
|
+
function collectSections(lines) {
|
|
529
|
+
const sections = new Map;
|
|
530
|
+
let current = null;
|
|
531
|
+
let content = [];
|
|
532
|
+
for (const line of lines) {
|
|
533
|
+
const heading = matchSectionHeading(line);
|
|
534
|
+
if (heading === null) {
|
|
535
|
+
if (current !== null) {
|
|
536
|
+
content.push(line);
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (current !== null) {
|
|
541
|
+
sections.set(current, joinContent(content));
|
|
542
|
+
}
|
|
543
|
+
if (!SECTION_NAMES.includes(heading)) {
|
|
544
|
+
return { ok: false, code: "UNKNOWN_SECTION" };
|
|
545
|
+
}
|
|
546
|
+
if (sections.has(heading)) {
|
|
547
|
+
return { ok: false, code: "DUPLICATE_SECTION" };
|
|
548
|
+
}
|
|
549
|
+
current = heading;
|
|
550
|
+
content = [];
|
|
551
|
+
}
|
|
552
|
+
if (current !== null) {
|
|
553
|
+
sections.set(current, joinContent(content));
|
|
554
|
+
}
|
|
555
|
+
return { ok: true, value: sections };
|
|
556
|
+
}
|
|
557
|
+
function validateSections(sections) {
|
|
558
|
+
const rule = sections.get("Rule");
|
|
559
|
+
const violation = sections.get("Violation");
|
|
560
|
+
const allowed = sections.get("Allowed") ?? null;
|
|
561
|
+
if (rule === undefined) {
|
|
562
|
+
return { ok: false, code: "MISSING_RULE" };
|
|
563
|
+
}
|
|
564
|
+
if (violation === undefined) {
|
|
565
|
+
return { ok: false, code: "MISSING_VIOLATION" };
|
|
566
|
+
}
|
|
567
|
+
if (rule === "") {
|
|
568
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
569
|
+
}
|
|
570
|
+
if (violation === "") {
|
|
571
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
572
|
+
}
|
|
573
|
+
if (allowed === "") {
|
|
574
|
+
return { ok: false, code: "EMPTY_ALLOWED" };
|
|
575
|
+
}
|
|
576
|
+
if (allowed !== null && allowed === violation) {
|
|
577
|
+
return { ok: false, code: "ALLOWED_EQUALS_VIOLATION" };
|
|
578
|
+
}
|
|
579
|
+
return { ok: true, value: { rule, violation, allowed } };
|
|
580
|
+
}
|
|
581
|
+
function joinContent(lines) {
|
|
582
|
+
return lines.join(`
|
|
583
|
+
`).trim();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// packages/core/src/rules/blocks.ts
|
|
587
|
+
var RULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
588
|
+
function isValidRuleId(id) {
|
|
589
|
+
return RULE_ID.test(id);
|
|
590
|
+
}
|
|
591
|
+
function collectRuleBlocks(text) {
|
|
592
|
+
const blocks = [];
|
|
593
|
+
let current = null;
|
|
594
|
+
for (const line of splitLines(text)) {
|
|
595
|
+
const id = matchRuleHeading(line);
|
|
596
|
+
if (id !== null) {
|
|
597
|
+
current = { id, body: [] };
|
|
598
|
+
blocks.push(current);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (current !== null) {
|
|
602
|
+
current.body.push(line);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return blocks;
|
|
606
|
+
}
|
|
607
|
+
function parseRuleBlock(block, duplicate) {
|
|
608
|
+
if (!isValidRuleId(block.id)) {
|
|
609
|
+
return ruleCandidateFailure("INVALID_ID", null);
|
|
610
|
+
}
|
|
611
|
+
if (duplicate) {
|
|
612
|
+
return ruleCandidateFailure("DUPLICATE_ID", block.id);
|
|
613
|
+
}
|
|
614
|
+
const metadata = parseRuleMetadata(block.body);
|
|
615
|
+
if (!metadata.ok) {
|
|
616
|
+
return ruleCandidateFailure(metadata.code, block.id);
|
|
617
|
+
}
|
|
618
|
+
const sections = parseRuleSections(block.body);
|
|
619
|
+
if (!sections.ok) {
|
|
620
|
+
return ruleCandidateFailure(sections.code, block.id);
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
status: "PARSED",
|
|
624
|
+
rule: buildParsedRule(block.id, metadata.value, sections.value)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function ruleCandidateFailure(code, ruleId) {
|
|
628
|
+
return { status: "INVALID", reason: "INVALID_RULE", code, ruleId };
|
|
629
|
+
}
|
|
630
|
+
function splitLines(text) {
|
|
631
|
+
return text.replace(/\r\n?/g, `
|
|
632
|
+
`).split(`
|
|
633
|
+
`);
|
|
634
|
+
}
|
|
635
|
+
function buildParsedRule(id, metadata, sections) {
|
|
636
|
+
return {
|
|
637
|
+
id,
|
|
638
|
+
severity: metadata.severity,
|
|
639
|
+
scope: metadata.scope,
|
|
640
|
+
description: sections.rule,
|
|
641
|
+
violation: sections.violation,
|
|
642
|
+
allowed: sections.allowed
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// packages/core/src/rules/parse-rules.ts
|
|
646
|
+
function parseRules(text) {
|
|
647
|
+
const blocks = collectRuleBlocks(text);
|
|
648
|
+
if (blocks.length === 0) {
|
|
649
|
+
return [ruleCandidateFailure("MISSING_ID", null)];
|
|
650
|
+
}
|
|
651
|
+
const duplicates = findDuplicateIds(blocks.map((block) => block.id));
|
|
652
|
+
return blocks.map((block) => parseRuleBlock(block, duplicates.has(block.id)));
|
|
653
|
+
}
|
|
654
|
+
function findDuplicateIds(ids) {
|
|
655
|
+
const counts = new Map;
|
|
656
|
+
for (const id of ids) {
|
|
657
|
+
if (!isValidRuleId(id)) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
661
|
+
}
|
|
662
|
+
const duplicates = new Set;
|
|
663
|
+
for (const [id, count] of counts) {
|
|
664
|
+
if (count > 1) {
|
|
665
|
+
duplicates.add(id);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return duplicates;
|
|
669
|
+
}
|
|
670
|
+
// packages/core/src/builtins/complexity.ts
|
|
671
|
+
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
672
|
+
var COMPLEXITY_GATE_CONFIG = {
|
|
673
|
+
version: 1,
|
|
674
|
+
thresholds: {
|
|
675
|
+
error: { warn: 0.4, fail: 0.7 },
|
|
676
|
+
warning: { warn: 0.5 }
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
var COMPLEXITY_INSTRUCTIONS = "Does this attributed change introduce material complexity that is disproportionate to, or not reasonably necessary for, completing the user's task? Necessary supporting changes, tests, validation, error handling, and complexity explicitly required by the task should not count as unnecessary complexity. Return the probability from 0 to 1 that the change introduces disproportionate or unnecessary complexity.";
|
|
680
|
+
var COMPLEXITY_DESCRIPTION = "Material complexity introduced by the attributed change that is disproportionate to the problem and not reasonably necessary for completing the task.";
|
|
681
|
+
var COMPLEXITY_VIOLATION = "The change adds unnecessary abstractions, layers or indirections without proportional gain, new dependencies without clear need, excessive configuration, premature generalization, or structure materially larger than the problem requires.";
|
|
682
|
+
var COMPLEXITY_ALLOWED = "Complexity explicitly required by the task, and necessary supporting changes, tests, validation, error handling, auxiliary changes, or following an existing codebase abstraction to avoid breaking its pattern.";
|
|
683
|
+
function buildComplexityQuestion() {
|
|
684
|
+
return {
|
|
685
|
+
type: "noul",
|
|
686
|
+
instructions: COMPLEXITY_INSTRUCTIONS,
|
|
687
|
+
criteria: {
|
|
688
|
+
id: COMPLEXITY_CHECK_ID,
|
|
689
|
+
description: COMPLEXITY_DESCRIPTION,
|
|
690
|
+
violation: COMPLEXITY_VIOLATION,
|
|
691
|
+
allowed: COMPLEXITY_ALLOWED
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function buildComplexityContext(turn, scopedPaths) {
|
|
696
|
+
return {
|
|
697
|
+
kind: "BUILT_IN",
|
|
698
|
+
turnId: turn.id,
|
|
699
|
+
ruleId: null,
|
|
700
|
+
checkId: COMPLEXITY_CHECK_ID,
|
|
701
|
+
severity: "warning",
|
|
702
|
+
scopedPaths
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// packages/core/src/builtins/scope-creep.ts
|
|
707
|
+
var SCOPE_CREEP_CHECK_ID = "SCOPE-CREEP";
|
|
708
|
+
var SCOPE_CREEP_GATE_CONFIG = {
|
|
709
|
+
version: 1,
|
|
710
|
+
thresholds: {
|
|
711
|
+
error: { warn: 0.65, fail: 0.9 },
|
|
712
|
+
warning: { warn: 0.6 }
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
var SCOPE_CREEP_INSTRUCTIONS = "Answer true when the attributed change contains scope creep: material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task. Answer false when every change is requested work, or supporting, testing, or documentation work reasonably necessary to complete the requested work, including immaterial incidental edits. Return the probability from 0 to 1 that the change is scope creep.";
|
|
716
|
+
var SCOPE_CREEP_DESCRIPTION = "A change is scope creep when it introduces material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete it.";
|
|
717
|
+
var SCOPE_CREEP_VIOLATION = "The change adds material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task.";
|
|
718
|
+
var SCOPE_CREEP_ALLOWED = "Changes the task requested, and supporting, testing, and documentation changes reasonably necessary to complete the requested work, including immaterial incidental edits.";
|
|
719
|
+
function buildScopeCreepQuestion() {
|
|
720
|
+
return {
|
|
721
|
+
type: "noul",
|
|
722
|
+
instructions: SCOPE_CREEP_INSTRUCTIONS,
|
|
723
|
+
criteria: {
|
|
724
|
+
id: SCOPE_CREEP_CHECK_ID,
|
|
725
|
+
description: SCOPE_CREEP_DESCRIPTION,
|
|
726
|
+
violation: SCOPE_CREEP_VIOLATION,
|
|
727
|
+
allowed: SCOPE_CREEP_ALLOWED
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function buildScopeCreepContext(turn, scopedPaths) {
|
|
732
|
+
return {
|
|
733
|
+
kind: "BUILT_IN",
|
|
734
|
+
turnId: turn.id,
|
|
735
|
+
ruleId: null,
|
|
736
|
+
checkId: SCOPE_CREEP_CHECK_ID,
|
|
737
|
+
severity: "error",
|
|
738
|
+
scopedPaths
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// packages/core/src/builtins/evaluate-builtins.ts
|
|
743
|
+
function buildBuiltInBatchRequest(task, evidence) {
|
|
744
|
+
return {
|
|
745
|
+
kind: "BUILT_IN_BATCH",
|
|
746
|
+
task,
|
|
747
|
+
questions: {
|
|
748
|
+
[SCOPE_CREEP_ANSWER]: buildScopeCreepQuestion(),
|
|
749
|
+
[COMPLEXITY_ANSWER]: buildComplexityQuestion()
|
|
750
|
+
},
|
|
751
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function evaluateBuiltIns(input, dependencies) {
|
|
755
|
+
const selection = selectTurnEvidence(input.turn, input.evidencePolicy);
|
|
756
|
+
if (selection.status === "SKIPPED") {
|
|
757
|
+
return {
|
|
758
|
+
scopeCreep: {
|
|
759
|
+
...buildScopeCreepContext(input.turn, []),
|
|
760
|
+
outcome: "SKIPPED",
|
|
761
|
+
reason: selection.reason
|
|
762
|
+
},
|
|
763
|
+
complexity: {
|
|
764
|
+
...buildComplexityContext(input.turn, []),
|
|
765
|
+
outcome: "SKIPPED",
|
|
766
|
+
reason: selection.reason
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (selection.status === "UNAVAILABLE") {
|
|
771
|
+
return {
|
|
772
|
+
scopeCreep: {
|
|
773
|
+
...buildScopeCreepContext(input.turn, []),
|
|
774
|
+
outcome: "UNAVAILABLE",
|
|
775
|
+
reason: selection.reason
|
|
776
|
+
},
|
|
777
|
+
complexity: {
|
|
778
|
+
...buildComplexityContext(input.turn, []),
|
|
779
|
+
outcome: "UNAVAILABLE",
|
|
780
|
+
reason: selection.reason
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const evidence = selection.evidence;
|
|
785
|
+
const request = buildBuiltInBatchRequest(input.turn.task, evidence);
|
|
786
|
+
const evaluation = await evaluateBuiltInBatchWithPort(dependencies.jev, request);
|
|
787
|
+
const scopeCreepContext = buildScopeCreepContext(input.turn, evidence.files);
|
|
788
|
+
const complexityContext = buildComplexityContext(input.turn, evidence.files);
|
|
789
|
+
if (evaluation.status === "FAILED") {
|
|
790
|
+
return {
|
|
791
|
+
scopeCreep: { ...scopeCreepContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" },
|
|
792
|
+
complexity: { ...complexityContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" }
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
scopeCreep: gateScopeCreep(evaluation.answers.scopeCreep, scopeCreepContext),
|
|
797
|
+
complexity: gateComplexity(evaluation.answers.complexity, complexityContext)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function gateScopeCreep(answer, context) {
|
|
801
|
+
if (answer.status === "FAILED") {
|
|
802
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
...context,
|
|
806
|
+
...evaluateGate("error", answer.noul.violationProbability, SCOPE_CREEP_GATE_CONFIG)
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function gateComplexity(answer, context) {
|
|
810
|
+
if (answer.status === "FAILED") {
|
|
811
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
...context,
|
|
815
|
+
...evaluateGate("warning", answer.noul.violationProbability, COMPLEXITY_GATE_CONFIG)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
// packages/core/src/concurrency/fifo-port.ts
|
|
819
|
+
function createFifoJevPort(port, config) {
|
|
820
|
+
const maxConcurrency = config.maxConcurrency;
|
|
821
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
822
|
+
throw new RangeError("maxConcurrency must be a positive integer");
|
|
823
|
+
}
|
|
824
|
+
const queue = [];
|
|
825
|
+
let active = 0;
|
|
826
|
+
function drain() {
|
|
827
|
+
while (active < maxConcurrency && queue.length > 0) {
|
|
828
|
+
const pending = queue.shift();
|
|
829
|
+
if (pending === undefined) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
active += 1;
|
|
833
|
+
start(pending);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function start(pending) {
|
|
837
|
+
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
838
|
+
active -= 1;
|
|
839
|
+
pending.settle(result);
|
|
840
|
+
drain();
|
|
841
|
+
}, (error) => {
|
|
842
|
+
active -= 1;
|
|
843
|
+
pending.fail(error);
|
|
844
|
+
drain();
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
evaluate(request) {
|
|
849
|
+
return new Promise((settle, fail) => {
|
|
850
|
+
queue.push({ request, settle, fail });
|
|
851
|
+
drain();
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
// packages/core/src/gate/defaults.ts
|
|
857
|
+
var DEFAULT_GATE_CONFIG = {
|
|
858
|
+
version: 1,
|
|
859
|
+
thresholds: {
|
|
860
|
+
error: { warn: 0.4, fail: 0.7 },
|
|
861
|
+
warning: { warn: 0.6 }
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
// packages/core/src/gate/config.ts
|
|
865
|
+
function resolveGateConfig(value) {
|
|
866
|
+
if (value === null || value === undefined) {
|
|
867
|
+
return { status: "VALID", config: DEFAULT_GATE_CONFIG };
|
|
868
|
+
}
|
|
869
|
+
if (!isRecord(value) || value.version !== 1) {
|
|
870
|
+
return invalid();
|
|
871
|
+
}
|
|
872
|
+
const thresholds = value.thresholds;
|
|
873
|
+
if (!isRecord(thresholds)) {
|
|
874
|
+
return invalid();
|
|
875
|
+
}
|
|
876
|
+
const error = thresholds.error;
|
|
877
|
+
const warning = thresholds.warning;
|
|
878
|
+
if (!isRecord(error) || !isRecord(warning)) {
|
|
879
|
+
return invalid();
|
|
880
|
+
}
|
|
881
|
+
const errorWarn = error.warn;
|
|
882
|
+
const errorFail = error.fail;
|
|
883
|
+
const warningWarn = warning.warn;
|
|
884
|
+
if (!isProbability(errorWarn) || !isProbability(errorFail) || !isProbability(warningWarn)) {
|
|
885
|
+
return invalid();
|
|
886
|
+
}
|
|
887
|
+
if (errorWarn >= errorFail) {
|
|
888
|
+
return invalid();
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
status: "VALID",
|
|
892
|
+
config: {
|
|
893
|
+
version: 1,
|
|
894
|
+
thresholds: {
|
|
895
|
+
error: { warn: errorWarn, fail: errorFail },
|
|
896
|
+
warning: { warn: warningWarn }
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function isRecord(value) {
|
|
902
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
903
|
+
}
|
|
904
|
+
function invalid() {
|
|
905
|
+
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
906
|
+
}
|
|
907
|
+
// packages/plugin/src/skills/jevguard-rules/rules-summary.ts
|
|
908
|
+
function summarizeRules(text) {
|
|
909
|
+
const results = parseRules(text);
|
|
910
|
+
const ruleIds = [];
|
|
911
|
+
const invalidRuleIds = [];
|
|
912
|
+
const errorCodes = [];
|
|
913
|
+
for (const result of results) {
|
|
914
|
+
if (result.status === "PARSED") {
|
|
915
|
+
ruleIds.push(result.rule.id);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
invalidRuleIds.push(result.ruleId);
|
|
919
|
+
errorCodes.push(result.code);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
status: invalidRuleIds.length === 0 ? "valid" : "invalid",
|
|
923
|
+
ruleCount: results.length,
|
|
924
|
+
ruleIds,
|
|
925
|
+
invalidCount: invalidRuleIds.length,
|
|
926
|
+
invalidRuleIds,
|
|
927
|
+
errorCodes: [...new Set(errorCodes)]
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// packages/plugin/src/skills/jevguard-rules/validate-rules.ts
|
|
932
|
+
var MAX_DOCUMENT_BYTES = 1048576;
|
|
933
|
+
var EXIT_VALID = 0;
|
|
934
|
+
var EXIT_INVALID = 1;
|
|
935
|
+
var EXIT_ERROR = 2;
|
|
936
|
+
var EXPECTED_ARGUMENT_COUNT = 1;
|
|
937
|
+
function print(report, exitCode) {
|
|
938
|
+
process.stdout.write(`${JSON.stringify(report)}
|
|
939
|
+
`);
|
|
940
|
+
return exitCode;
|
|
941
|
+
}
|
|
942
|
+
function fail(errorCode) {
|
|
943
|
+
return print({ status: "error", errorCode }, EXIT_ERROR);
|
|
944
|
+
}
|
|
945
|
+
function resolvePath(requested) {
|
|
946
|
+
try {
|
|
947
|
+
return resolve(requested);
|
|
948
|
+
} catch {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function readDocument(path) {
|
|
953
|
+
let size;
|
|
954
|
+
try {
|
|
955
|
+
const stats = statSync(path);
|
|
956
|
+
if (!stats.isFile()) {
|
|
957
|
+
return { ok: false, code: "NOT_A_FILE" };
|
|
958
|
+
}
|
|
959
|
+
size = stats.size;
|
|
960
|
+
} catch {
|
|
961
|
+
return { ok: false, code: "NOT_A_FILE" };
|
|
962
|
+
}
|
|
963
|
+
if (size > MAX_DOCUMENT_BYTES) {
|
|
964
|
+
return { ok: false, code: "FILE_TOO_LARGE" };
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
return { ok: true, text: readFileSync(path, "utf8") };
|
|
968
|
+
} catch {
|
|
969
|
+
return { ok: false, code: "READ_FAILURE" };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function main(argv) {
|
|
973
|
+
if (argv.length !== EXPECTED_ARGUMENT_COUNT) {
|
|
974
|
+
return fail("EXPECTED_ONE_PATH_ARGUMENT");
|
|
975
|
+
}
|
|
976
|
+
const requested = argv[0];
|
|
977
|
+
if (requested === undefined || requested.length === 0) {
|
|
978
|
+
return fail("EXPECTED_ONE_PATH_ARGUMENT");
|
|
979
|
+
}
|
|
980
|
+
const path = resolvePath(requested);
|
|
981
|
+
if (path === null) {
|
|
982
|
+
return fail("INVALID_PATH");
|
|
983
|
+
}
|
|
984
|
+
const document = readDocument(path);
|
|
985
|
+
if (!document.ok) {
|
|
986
|
+
return fail(document.code);
|
|
987
|
+
}
|
|
988
|
+
const summary = summarizeRules(document.text);
|
|
989
|
+
return print(summary, summary.status === "valid" ? EXIT_VALID : EXIT_INVALID);
|
|
990
|
+
}
|
|
991
|
+
process.exitCode = main(process.argv.slice(2));
|