@pablozrrrr/jevguard 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -19
- package/cli/main.js +1299 -88
- package/index.js +1174 -96
- package/package.json +1 -1
- package/skills/jev-init/SKILL.md +12 -5
- package/skills/jev-init/validate-init.js +541 -68
- package/skills/jevguard-rules/SKILL.md +44 -2
- package/skills/jevguard-rules/validate-rules.js +541 -68
|
@@ -39,7 +39,7 @@ function buildReviewContext(turn, rule, scopedPaths) {
|
|
|
39
39
|
var DEFAULT_MAX_DIFF_LENGTH = 1e5;
|
|
40
40
|
var DEFAULT_EVIDENCE_POLICY = {
|
|
41
41
|
maxDiffLength: DEFAULT_MAX_DIFF_LENGTH,
|
|
42
|
-
|
|
42
|
+
codeExtensions: [
|
|
43
43
|
".ts",
|
|
44
44
|
".tsx",
|
|
45
45
|
".mts",
|
|
@@ -50,9 +50,6 @@ var DEFAULT_EVIDENCE_POLICY = {
|
|
|
50
50
|
".cjs",
|
|
51
51
|
".json",
|
|
52
52
|
".jsonc",
|
|
53
|
-
".md",
|
|
54
|
-
".mdx",
|
|
55
|
-
".txt",
|
|
56
53
|
".yaml",
|
|
57
54
|
".yml",
|
|
58
55
|
".toml",
|
|
@@ -108,6 +105,7 @@ var DEFAULT_EVIDENCE_POLICY = {
|
|
|
108
105
|
".diff",
|
|
109
106
|
".patch"
|
|
110
107
|
],
|
|
108
|
+
docsExtensions: [".md", ".mdx", ".txt"],
|
|
111
109
|
deniedFileNames: [
|
|
112
110
|
".env",
|
|
113
111
|
".envrc",
|
|
@@ -182,18 +180,26 @@ function checkFilePath(path, policy) {
|
|
|
182
180
|
if (policy.deniedExtensions.includes(extension)) {
|
|
183
181
|
return DENIED_SENSITIVE;
|
|
184
182
|
}
|
|
185
|
-
if (!
|
|
183
|
+
if (!isAllowlistedExtension(extension, policy)) {
|
|
186
184
|
return DENIED_EXTENSION;
|
|
187
185
|
}
|
|
188
186
|
return { allowed: true };
|
|
189
187
|
}
|
|
188
|
+
function isAllowlistedExtension(extension, policy) {
|
|
189
|
+
return policy.codeExtensions.includes(extension) || policy.docsExtensions.includes(extension);
|
|
190
|
+
}
|
|
190
191
|
function matchesDeniedFileName(basename, policy) {
|
|
191
192
|
return policy.deniedFileNames.some((name) => name.startsWith(".") ? basename === name || basename.startsWith(`${name}.`) : basename === name);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
// packages/core/src/evidence/scope.ts
|
|
195
|
-
function
|
|
196
|
-
|
|
196
|
+
function matchesRuleScope(path, scope) {
|
|
197
|
+
const normalized = normalizePath(path);
|
|
198
|
+
const included = scope.include.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
199
|
+
if (!included) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return !scope.exclude.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
197
203
|
}
|
|
198
204
|
function globToRegExp(glob) {
|
|
199
205
|
const source = glob.replace(/\\/g, "/");
|
|
@@ -220,16 +226,6 @@ function globToRegExp(glob) {
|
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
// 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
229
|
function selectTurnEvidence(turn, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
234
230
|
if (hasNoAttributedPatch(turn)) {
|
|
235
231
|
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
@@ -256,11 +252,21 @@ function mountEvidence(files, policy) {
|
|
|
256
252
|
function hasNoAttributedPatch(turn) {
|
|
257
253
|
return turn.files.every((file) => file.patch.trim() === "");
|
|
258
254
|
}
|
|
259
|
-
function selectApplicableFiles(files, scope) {
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
function selectApplicableFiles(files, scope, evidenceClass, policy) {
|
|
256
|
+
const scoped = scope === null ? files : files.filter((file) => matchesRuleScope(file.path, scope));
|
|
257
|
+
return scoped.filter((file) => isInEvidenceClass(file.path, evidenceClass, policy));
|
|
258
|
+
}
|
|
259
|
+
function isInEvidenceClass(path, evidenceClass, policy) {
|
|
260
|
+
if (evidenceClass === "any") {
|
|
261
|
+
return true;
|
|
262
262
|
}
|
|
263
|
-
|
|
263
|
+
const safety = checkFilePath(path, policy);
|
|
264
|
+
if (!safety.allowed && safety.reason === "DENIED_SENSITIVE_PATH") {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
const extension = fileExtension(path);
|
|
268
|
+
const otherClass = evidenceClass === "code" ? policy.docsExtensions : policy.codeExtensions;
|
|
269
|
+
return !otherClass.includes(extension);
|
|
264
270
|
}
|
|
265
271
|
function assembleDiff(files) {
|
|
266
272
|
return files.map((file) => file.patch).join(`
|
|
@@ -301,14 +307,20 @@ function evaluateWarningGate(probability, thresholds) {
|
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
// packages/core/src/evaluation/evaluate-with-port.ts
|
|
304
|
-
async function
|
|
310
|
+
async function evaluateRuleBatchWithPort(port, requests) {
|
|
305
311
|
try {
|
|
306
|
-
const
|
|
307
|
-
|
|
312
|
+
const results = await port.evaluateRuleBatch(requests);
|
|
313
|
+
if (results.length !== requests.length) {
|
|
314
|
+
return requests.map(() => ruleFailure("INVALID_RESPONSE"));
|
|
315
|
+
}
|
|
316
|
+
return results.map((result) => result.kind === "RULE" ? result : ruleFailure("INVALID_RESPONSE"));
|
|
308
317
|
} catch {
|
|
309
|
-
return
|
|
318
|
+
return requests.map(() => ruleFailure("API_ERROR"));
|
|
310
319
|
}
|
|
311
320
|
}
|
|
321
|
+
function ruleFailure(reason) {
|
|
322
|
+
return { kind: "RULE", status: "FAILED", reason };
|
|
323
|
+
}
|
|
312
324
|
async function evaluateBuiltInBatchWithPort(port, request) {
|
|
313
325
|
try {
|
|
314
326
|
const result = await port.evaluate(request);
|
|
@@ -317,27 +329,118 @@ async function evaluateBuiltInBatchWithPort(port, request) {
|
|
|
317
329
|
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "API_ERROR" };
|
|
318
330
|
}
|
|
319
331
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const context = buildReviewContext(input.turn, input.rule, []);
|
|
326
|
-
return { ...context, outcome: "SKIPPED", reason: selection.reason };
|
|
332
|
+
// packages/core/src/evidence/plan-slices.ts
|
|
333
|
+
var HUNK_MARKER = /^@@ [^\n]*@@/gm;
|
|
334
|
+
function planRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
335
|
+
if (hasNoAttributedPatch(turn)) {
|
|
336
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
327
337
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return {
|
|
338
|
+
const applicableFiles = selectApplicableFiles(turn.files, rule.scope, rule.evidence, policy);
|
|
339
|
+
if (applicableFiles.length === 0) {
|
|
340
|
+
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
331
341
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
342
|
+
const diff = assembleDiff(applicableFiles);
|
|
343
|
+
if (diff.trim() === "") {
|
|
344
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
345
|
+
}
|
|
346
|
+
if (applicableFiles.some((file) => !checkFilePath(file.path, policy).allowed)) {
|
|
347
|
+
return { status: "UNAVAILABLE", reason: "BLOCKED_EVIDENCE" };
|
|
348
|
+
}
|
|
349
|
+
if (diff.length <= policy.maxDiffLength) {
|
|
350
|
+
return { status: "PLANNED", slices: [wholeSlice(applicableFiles, diff)] };
|
|
351
|
+
}
|
|
352
|
+
return sliceByFile(applicableFiles, policy);
|
|
353
|
+
}
|
|
354
|
+
function wholeSlice(files, diff) {
|
|
355
|
+
const paths = files.map((file) => file.path);
|
|
356
|
+
return {
|
|
357
|
+
index: 0,
|
|
358
|
+
kind: "WHOLE",
|
|
359
|
+
path: paths.length === 1 ? paths[0] ?? null : null,
|
|
360
|
+
hunkOrdinal: null,
|
|
361
|
+
files: paths,
|
|
362
|
+
diff
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function sliceByFile(files, policy) {
|
|
366
|
+
const slices = [];
|
|
367
|
+
for (const file of files) {
|
|
368
|
+
if (file.patch.length <= policy.maxDiffLength) {
|
|
369
|
+
slices.push(fileSlice(slices.length, file));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const hunks = sliceHunks(slices.length, file, policy);
|
|
373
|
+
if (hunks === null) {
|
|
374
|
+
return { status: "UNAVAILABLE", reason: "OVERSIZED_DIFF" };
|
|
375
|
+
}
|
|
376
|
+
slices.push(...hunks);
|
|
377
|
+
}
|
|
378
|
+
return { status: "PLANNED", slices };
|
|
379
|
+
}
|
|
380
|
+
function fileSlice(index, file) {
|
|
381
|
+
return {
|
|
382
|
+
index,
|
|
383
|
+
kind: "FILE",
|
|
384
|
+
path: file.path,
|
|
385
|
+
hunkOrdinal: null,
|
|
386
|
+
files: [file.path],
|
|
387
|
+
diff: file.patch
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function sliceHunks(startIndex, file, policy) {
|
|
391
|
+
const starts = hunkStarts(file.patch);
|
|
392
|
+
if (starts.length === 0) {
|
|
393
|
+
return null;
|
|
337
394
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
395
|
+
const firstStart = starts[0];
|
|
396
|
+
if (firstStart === undefined) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const preamble = file.patch.slice(0, firstStart);
|
|
400
|
+
const slices = [];
|
|
401
|
+
for (let ordinal = 0;ordinal < starts.length; ordinal += 1) {
|
|
402
|
+
const start = starts[ordinal];
|
|
403
|
+
if (start === undefined) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const end = starts[ordinal + 1] ?? file.patch.length;
|
|
407
|
+
const rendered = preamble + file.patch.slice(start, end);
|
|
408
|
+
if (rendered.length > policy.maxDiffLength) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
slices.push({
|
|
412
|
+
index: startIndex + ordinal,
|
|
413
|
+
kind: "HUNK",
|
|
414
|
+
path: file.path,
|
|
415
|
+
hunkOrdinal: ordinal,
|
|
416
|
+
files: [file.path],
|
|
417
|
+
diff: rendered
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return slices;
|
|
421
|
+
}
|
|
422
|
+
function hunkStarts(patch) {
|
|
423
|
+
return Array.from(patch.matchAll(HUNK_MARKER), (match) => match.index ?? 0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// packages/core/src/evidence/slice-budget.ts
|
|
427
|
+
var MAX_SLICES_PER_RULE = 16;
|
|
428
|
+
var MAX_EXTRA_RULE_CALLS_PER_TURN = 32;
|
|
429
|
+
function planTurnRuleSlices(plans) {
|
|
430
|
+
let remainingExtraCalls = MAX_EXTRA_RULE_CALLS_PER_TURN;
|
|
431
|
+
return plans.map((plan) => {
|
|
432
|
+
if (plan.status !== "PLANNED") {
|
|
433
|
+
return plan;
|
|
434
|
+
}
|
|
435
|
+
const extraCalls = plan.slices.length - 1;
|
|
436
|
+
if (plan.slices.length > MAX_SLICES_PER_RULE || extraCalls > remainingExtraCalls) {
|
|
437
|
+
return { status: "SLICE_LIMIT_EXCEEDED", slices: plan.slices };
|
|
438
|
+
}
|
|
439
|
+
remainingExtraCalls -= extraCalls;
|
|
440
|
+
return plan;
|
|
441
|
+
});
|
|
340
442
|
}
|
|
443
|
+
|
|
341
444
|
// packages/core/src/review/aggregate.ts
|
|
342
445
|
var VERDICT_RANK = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
343
446
|
function aggregateReview(turnId, results) {
|
|
@@ -391,28 +494,204 @@ function isSemanticVerdict(outcome) {
|
|
|
391
494
|
return outcome === "PASS" || outcome === "WARN" || outcome === "FAIL";
|
|
392
495
|
}
|
|
393
496
|
|
|
394
|
-
// packages/core/src/evaluation/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
497
|
+
// packages/core/src/evaluation/assemble-rule.ts
|
|
498
|
+
var VERDICT_RANK2 = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
499
|
+
function assembleSlicedRuleResult(rule, context, slices, evaluations, gateConfig) {
|
|
500
|
+
if (slices.length === 0) {
|
|
501
|
+
return { ...context, outcome: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
502
|
+
}
|
|
503
|
+
const summaries = slices.map(sliceSummary);
|
|
504
|
+
const mode = evidenceMode(slices.length);
|
|
505
|
+
const gates = [];
|
|
506
|
+
const judgments = [];
|
|
507
|
+
let failureIndex = null;
|
|
508
|
+
for (let index = 0;index < slices.length; index += 1) {
|
|
509
|
+
const slice = slices[index];
|
|
510
|
+
const evaluation = evaluations[index];
|
|
511
|
+
if (slice === undefined || evaluation === undefined || evaluation.status === "FAILED") {
|
|
512
|
+
failureIndex = index;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
const gate = evaluateGate(rule.severity, evaluation.noul.violationProbability, gateConfig);
|
|
516
|
+
if (gate.outcome === "UNAVAILABLE") {
|
|
517
|
+
failureIndex = index;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
gates.push(gate);
|
|
521
|
+
judgments.push({
|
|
522
|
+
index: slice.index,
|
|
523
|
+
probability: gate.violationProbability,
|
|
524
|
+
outcome: gate.outcome
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (failureIndex !== null) {
|
|
528
|
+
return {
|
|
529
|
+
...context,
|
|
530
|
+
outcome: "UNAVAILABLE",
|
|
531
|
+
reason: "JEV_FAILURE",
|
|
532
|
+
evidence: failedMetadata(mode, summaries, failureIndex)
|
|
533
|
+
};
|
|
399
534
|
}
|
|
400
|
-
return
|
|
535
|
+
return {
|
|
536
|
+
...context,
|
|
537
|
+
outcome: mergeVerdict(gates),
|
|
538
|
+
violationProbability: maxProbability(gates),
|
|
539
|
+
evidence: completeMetadata(mode, summaries, judgments)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function overBudgetMetadata(slices) {
|
|
543
|
+
const summaries = slices.map(sliceSummary);
|
|
544
|
+
return {
|
|
545
|
+
mode: "SLICED",
|
|
546
|
+
plannedSliceCount: summaries.length,
|
|
547
|
+
evaluatedSliceCount: 0,
|
|
548
|
+
failedSliceCount: 0,
|
|
549
|
+
notEvaluatedSliceCount: summaries.length,
|
|
550
|
+
slices: summaries,
|
|
551
|
+
sliceJudgments: [],
|
|
552
|
+
failingSliceIndex: null,
|
|
553
|
+
failureReason: "SLICE_LIMIT_EXCEEDED"
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function sliceSummary(slice) {
|
|
557
|
+
return {
|
|
558
|
+
index: slice.index,
|
|
559
|
+
path: slice.path,
|
|
560
|
+
kind: slice.kind,
|
|
561
|
+
hunkOrdinal: slice.hunkOrdinal
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function evidenceMode(sliceCount) {
|
|
565
|
+
return sliceCount === 1 ? "WHOLE" : "SLICED";
|
|
401
566
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
567
|
+
function mergeVerdict(gates) {
|
|
568
|
+
let verdict = "PASS";
|
|
569
|
+
for (const gate of gates) {
|
|
570
|
+
if (VERDICT_RANK2[gate.outcome] > VERDICT_RANK2[verdict]) {
|
|
571
|
+
verdict = gate.outcome;
|
|
572
|
+
}
|
|
405
573
|
}
|
|
574
|
+
return verdict;
|
|
575
|
+
}
|
|
576
|
+
function maxProbability(gates) {
|
|
577
|
+
return gates.reduce((highest, gate) => Math.max(highest, gate.violationProbability), Number.NEGATIVE_INFINITY);
|
|
578
|
+
}
|
|
579
|
+
function completeMetadata(mode, summaries, judgments) {
|
|
580
|
+
return {
|
|
581
|
+
mode,
|
|
582
|
+
plannedSliceCount: summaries.length,
|
|
583
|
+
evaluatedSliceCount: summaries.length,
|
|
584
|
+
failedSliceCount: 0,
|
|
585
|
+
notEvaluatedSliceCount: 0,
|
|
586
|
+
slices: summaries,
|
|
587
|
+
sliceJudgments: judgments,
|
|
588
|
+
failingSliceIndex: null,
|
|
589
|
+
failureReason: null
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function failedMetadata(mode, summaries, failingIndex) {
|
|
593
|
+
return {
|
|
594
|
+
mode,
|
|
595
|
+
plannedSliceCount: summaries.length,
|
|
596
|
+
evaluatedSliceCount: failingIndex,
|
|
597
|
+
failedSliceCount: 1,
|
|
598
|
+
notEvaluatedSliceCount: summaries.length - failingIndex - 1,
|
|
599
|
+
slices: summaries,
|
|
600
|
+
sliceJudgments: [],
|
|
601
|
+
failingSliceIndex: failingIndex,
|
|
602
|
+
failureReason: "JEV_FAILURE"
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// packages/core/src/evaluation/evaluate-rules.ts
|
|
607
|
+
async function evaluateRules(input, dependencies) {
|
|
406
608
|
if (input.gateConfig.status === "INVALID") {
|
|
407
|
-
|
|
609
|
+
const results = input.rules.map((candidate) => candidate.status === "INVALID" ? invalidRuleResult(input.turn.id, candidate) : invalidConfigResult(input.turn, candidate.rule));
|
|
610
|
+
return aggregateReview(input.turn.id, results);
|
|
611
|
+
}
|
|
612
|
+
const gateConfig = input.gateConfig.config;
|
|
613
|
+
const slots = resolveSlots(input);
|
|
614
|
+
const requests = collectRequests(input.turn.task, slots);
|
|
615
|
+
const evaluations = requests.length === 0 ? [] : await evaluateRuleBatchWithPort(dependencies.jev, requests);
|
|
616
|
+
return aggregateReview(input.turn.id, assembleResults(input, slots, evaluations, gateConfig));
|
|
617
|
+
}
|
|
618
|
+
function resolveSlots(input) {
|
|
619
|
+
const planned = input.rules.map((candidate) => candidate.status === "INVALID" ? { kind: "RESULT", result: invalidRuleResult(input.turn.id, candidate) } : {
|
|
620
|
+
kind: "PLAN",
|
|
621
|
+
rule: candidate.rule,
|
|
622
|
+
plan: planRuleEvidence(input.turn, candidate.rule, input.evidencePolicy)
|
|
623
|
+
});
|
|
624
|
+
const budgeted = planTurnRuleSlices(planned.flatMap((slot) => slot.kind === "PLAN" ? [slot.plan] : []));
|
|
625
|
+
let cursor = 0;
|
|
626
|
+
return planned.map((slot) => {
|
|
627
|
+
if (slot.kind === "RESULT") {
|
|
628
|
+
return slot;
|
|
629
|
+
}
|
|
630
|
+
const plan = budgeted[cursor];
|
|
631
|
+
cursor += 1;
|
|
632
|
+
return plan === undefined ? slot : { kind: "PLAN", rule: slot.rule, plan };
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function collectRequests(task, slots) {
|
|
636
|
+
const requests = [];
|
|
637
|
+
for (const slot of slots) {
|
|
638
|
+
if (slot.kind !== "PLAN" || slot.plan.status !== "PLANNED") {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (const slice of slot.plan.slices) {
|
|
642
|
+
requests.push(buildJevRequest(task, slot.rule, { files: slice.files, diff: slice.diff }));
|
|
643
|
+
}
|
|
408
644
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
645
|
+
return requests;
|
|
646
|
+
}
|
|
647
|
+
function assembleResults(input, slots, evaluations, gateConfig) {
|
|
648
|
+
const results = [];
|
|
649
|
+
let evaluationCursor = 0;
|
|
650
|
+
for (const slot of slots) {
|
|
651
|
+
if (slot.kind === "RESULT") {
|
|
652
|
+
results.push(slot.result);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const plan = slot.plan;
|
|
656
|
+
if (plan.status === "SKIPPED") {
|
|
657
|
+
results.push(skippedResult(input.turn, slot.rule, plan.reason));
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (plan.status === "UNAVAILABLE") {
|
|
661
|
+
results.push(unavailableResult(input.turn, slot.rule, plan.reason));
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (plan.status === "SLICE_LIMIT_EXCEEDED") {
|
|
665
|
+
results.push({
|
|
666
|
+
...buildReviewContext(input.turn, slot.rule, collectSlicePaths(plan.slices)),
|
|
667
|
+
outcome: "UNAVAILABLE",
|
|
668
|
+
reason: "SLICE_LIMIT_EXCEEDED",
|
|
669
|
+
evidence: overBudgetMetadata(plan.slices)
|
|
670
|
+
});
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const sliceEvaluations = evaluations.slice(evaluationCursor, evaluationCursor + plan.slices.length);
|
|
674
|
+
evaluationCursor += plan.slices.length;
|
|
675
|
+
results.push(assembleSlicedRuleResult(slot.rule, buildReviewContext(input.turn, slot.rule, collectSlicePaths(plan.slices)), plan.slices, sliceEvaluations, gateConfig));
|
|
676
|
+
}
|
|
677
|
+
return results;
|
|
678
|
+
}
|
|
679
|
+
function collectSlicePaths(slices) {
|
|
680
|
+
const paths = [];
|
|
681
|
+
for (const slice of slices) {
|
|
682
|
+
for (const path of slice.files) {
|
|
683
|
+
if (!paths.includes(path)) {
|
|
684
|
+
paths.push(path);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return paths;
|
|
689
|
+
}
|
|
690
|
+
function skippedResult(turn, rule, reason) {
|
|
691
|
+
return { ...buildReviewContext(turn, rule, []), outcome: "SKIPPED", reason };
|
|
692
|
+
}
|
|
693
|
+
function unavailableResult(turn, rule, reason) {
|
|
694
|
+
return { ...buildReviewContext(turn, rule, []), outcome: "UNAVAILABLE", reason };
|
|
416
695
|
}
|
|
417
696
|
function invalidRuleResult(turnId, failure) {
|
|
418
697
|
return {
|
|
@@ -454,13 +733,29 @@ function matchSectionHeading(line) {
|
|
|
454
733
|
var METADATA_ENTRY = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
455
734
|
function parseRuleMetadata(lines) {
|
|
456
735
|
let severity = null;
|
|
457
|
-
let
|
|
736
|
+
let evidence = null;
|
|
737
|
+
let scopeDeclared = false;
|
|
738
|
+
const includePatterns = [];
|
|
739
|
+
const excludePatterns = [];
|
|
458
740
|
const seenKeys = new Set;
|
|
459
741
|
for (const line of collectMetadataLines(lines)) {
|
|
460
742
|
const entry = parseMetadataEntry(line);
|
|
461
743
|
if (entry === null) {
|
|
462
744
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
463
745
|
}
|
|
746
|
+
if (entry.key === "scope") {
|
|
747
|
+
const pattern = parseScopePattern(entry.value);
|
|
748
|
+
if (pattern === null) {
|
|
749
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
750
|
+
}
|
|
751
|
+
scopeDeclared = true;
|
|
752
|
+
if (pattern.exclude) {
|
|
753
|
+
excludePatterns.push(pattern.pattern);
|
|
754
|
+
} else {
|
|
755
|
+
includePatterns.push(pattern.pattern);
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
464
759
|
if (seenKeys.has(entry.key)) {
|
|
465
760
|
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
466
761
|
}
|
|
@@ -473,11 +768,12 @@ function parseRuleMetadata(lines) {
|
|
|
473
768
|
severity = parsed;
|
|
474
769
|
continue;
|
|
475
770
|
}
|
|
476
|
-
if (entry.key === "
|
|
477
|
-
|
|
771
|
+
if (entry.key === "evidence") {
|
|
772
|
+
const parsed = parseEvidenceClass(entry.value);
|
|
773
|
+
if (parsed === null) {
|
|
478
774
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
479
775
|
}
|
|
480
|
-
|
|
776
|
+
evidence = parsed;
|
|
481
777
|
continue;
|
|
482
778
|
}
|
|
483
779
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
@@ -485,7 +781,11 @@ function parseRuleMetadata(lines) {
|
|
|
485
781
|
if (severity === null) {
|
|
486
782
|
return { ok: false, code: "MISSING_SEVERITY" };
|
|
487
783
|
}
|
|
488
|
-
|
|
784
|
+
if (scopeDeclared && includePatterns.length === 0) {
|
|
785
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
786
|
+
}
|
|
787
|
+
const scope = scopeDeclared ? { include: includePatterns, exclude: excludePatterns } : null;
|
|
788
|
+
return { ok: true, value: { severity, scope, evidence: evidence ?? "code" } };
|
|
489
789
|
}
|
|
490
790
|
function collectMetadataLines(lines) {
|
|
491
791
|
const collected = [];
|
|
@@ -515,6 +815,26 @@ function parseSeverity(value) {
|
|
|
515
815
|
return null;
|
|
516
816
|
}
|
|
517
817
|
}
|
|
818
|
+
function parseEvidenceClass(value) {
|
|
819
|
+
switch (value) {
|
|
820
|
+
case "code":
|
|
821
|
+
case "docs":
|
|
822
|
+
case "any":
|
|
823
|
+
return value;
|
|
824
|
+
default:
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function parseScopePattern(value) {
|
|
829
|
+
if (value === "") {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
if (value.startsWith("!")) {
|
|
833
|
+
const pattern = value.slice(1).trim();
|
|
834
|
+
return pattern === "" ? null : { pattern, exclude: true };
|
|
835
|
+
}
|
|
836
|
+
return { pattern: value, exclude: false };
|
|
837
|
+
}
|
|
518
838
|
|
|
519
839
|
// packages/core/src/rules/sections.ts
|
|
520
840
|
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
@@ -637,6 +957,7 @@ function buildParsedRule(id, metadata, sections) {
|
|
|
637
957
|
id,
|
|
638
958
|
severity: metadata.severity,
|
|
639
959
|
scope: metadata.scope,
|
|
960
|
+
evidence: metadata.evidence,
|
|
640
961
|
description: sections.rule,
|
|
641
962
|
violation: sections.violation,
|
|
642
963
|
allowed: sections.allowed
|
|
@@ -667,6 +988,130 @@ function findDuplicateIds(ids) {
|
|
|
667
988
|
}
|
|
668
989
|
return duplicates;
|
|
669
990
|
}
|
|
991
|
+
// packages/core/src/report/aggregate.ts
|
|
992
|
+
function aggregateReviewHistory(records, skippedLineCount) {
|
|
993
|
+
const outcomes = emptyAccumulator();
|
|
994
|
+
const unavailableReasons = new Map;
|
|
995
|
+
const skippedReasons = new Map;
|
|
996
|
+
const builtIns = new Map;
|
|
997
|
+
const rules = new Map;
|
|
998
|
+
const evaluation = { wholeEvaluations: 0, slicedEvaluations: 0, totalSliceJudgments: 0 };
|
|
999
|
+
let firstTimestamp = null;
|
|
1000
|
+
let lastTimestamp = null;
|
|
1001
|
+
for (const record of records) {
|
|
1002
|
+
firstTimestamp = earlier(firstTimestamp, record.timestamp);
|
|
1003
|
+
lastTimestamp = later(lastTimestamp, record.timestamp);
|
|
1004
|
+
for (const result of record.results) {
|
|
1005
|
+
countOutcome(outcomes, result.outcome);
|
|
1006
|
+
if (result.outcome === "UNAVAILABLE") {
|
|
1007
|
+
bump(unavailableReasons, result.reason);
|
|
1008
|
+
}
|
|
1009
|
+
if (result.outcome === "SKIPPED") {
|
|
1010
|
+
bump(skippedReasons, result.reason);
|
|
1011
|
+
}
|
|
1012
|
+
if (result.kind === "BUILT_IN") {
|
|
1013
|
+
bump(builtIns, result.checkId);
|
|
1014
|
+
}
|
|
1015
|
+
if (result.kind === "RULE" && result.ruleId !== null) {
|
|
1016
|
+
countRule(rules, result.ruleId, result.outcome, result.evidence);
|
|
1017
|
+
}
|
|
1018
|
+
if (result.kind === "RULE" && result.evidence !== undefined) {
|
|
1019
|
+
countEvidence(evaluation, result.evidence.mode, result.evidence.sliceJudgments.length);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
entryCount: records.length,
|
|
1025
|
+
skippedLineCount,
|
|
1026
|
+
firstTimestamp,
|
|
1027
|
+
lastTimestamp,
|
|
1028
|
+
outcomes: finalizeAccumulator(outcomes),
|
|
1029
|
+
unavailableReasons: sortedReasons(unavailableReasons),
|
|
1030
|
+
skippedReasons: sortedReasons(skippedReasons),
|
|
1031
|
+
builtIns: sortedBuiltIns(builtIns),
|
|
1032
|
+
rules: sortedRules(rules),
|
|
1033
|
+
evaluation: finalizeEvaluation(evaluation)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function emptyAccumulator() {
|
|
1037
|
+
return { pass: 0, warn: 0, fail: 0, unavailable: 0, skipped: 0 };
|
|
1038
|
+
}
|
|
1039
|
+
function countOutcome(counts, outcome) {
|
|
1040
|
+
switch (outcome) {
|
|
1041
|
+
case "PASS":
|
|
1042
|
+
counts.pass += 1;
|
|
1043
|
+
return;
|
|
1044
|
+
case "WARN":
|
|
1045
|
+
counts.warn += 1;
|
|
1046
|
+
return;
|
|
1047
|
+
case "FAIL":
|
|
1048
|
+
counts.fail += 1;
|
|
1049
|
+
return;
|
|
1050
|
+
case "UNAVAILABLE":
|
|
1051
|
+
counts.unavailable += 1;
|
|
1052
|
+
return;
|
|
1053
|
+
case "SKIPPED":
|
|
1054
|
+
counts.skipped += 1;
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function countRule(rules, ruleId, outcome, evidence) {
|
|
1059
|
+
const accumulator = rules.get(ruleId) ?? { counts: emptyAccumulator(), sliced: 0 };
|
|
1060
|
+
countOutcome(accumulator.counts, outcome);
|
|
1061
|
+
if (evidence?.mode === "SLICED") {
|
|
1062
|
+
accumulator.sliced += 1;
|
|
1063
|
+
}
|
|
1064
|
+
rules.set(ruleId, accumulator);
|
|
1065
|
+
}
|
|
1066
|
+
function countEvidence(coverage, mode, sliceJudgmentCount) {
|
|
1067
|
+
if (mode === "WHOLE") {
|
|
1068
|
+
coverage.wholeEvaluations += 1;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
coverage.slicedEvaluations += 1;
|
|
1072
|
+
coverage.totalSliceJudgments += sliceJudgmentCount;
|
|
1073
|
+
}
|
|
1074
|
+
function finalizeAccumulator(counts) {
|
|
1075
|
+
return {
|
|
1076
|
+
pass: counts.pass,
|
|
1077
|
+
warn: counts.warn,
|
|
1078
|
+
fail: counts.fail,
|
|
1079
|
+
unavailable: counts.unavailable,
|
|
1080
|
+
skipped: counts.skipped,
|
|
1081
|
+
total: counts.pass + counts.warn + counts.fail + counts.unavailable + counts.skipped
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function finalizeEvaluation(coverage) {
|
|
1085
|
+
return { ...coverage };
|
|
1086
|
+
}
|
|
1087
|
+
function bump(map, key) {
|
|
1088
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
1089
|
+
}
|
|
1090
|
+
function sortedReasons(map) {
|
|
1091
|
+
return [...map.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareStrings(left.reason, right.reason));
|
|
1092
|
+
}
|
|
1093
|
+
function sortedBuiltIns(map) {
|
|
1094
|
+
return [...map.entries()].map(([checkId, count]) => ({ checkId, count })).sort((left, right) => compareStrings(left.checkId, right.checkId));
|
|
1095
|
+
}
|
|
1096
|
+
function sortedRules(map) {
|
|
1097
|
+
return [...map.entries()].map(([ruleId, accumulator]) => ({
|
|
1098
|
+
ruleId,
|
|
1099
|
+
counts: finalizeAccumulator(accumulator.counts),
|
|
1100
|
+
slicedRuleCount: accumulator.sliced
|
|
1101
|
+
})).sort((left, right) => compareStrings(left.ruleId, right.ruleId));
|
|
1102
|
+
}
|
|
1103
|
+
function earlier(current, candidate) {
|
|
1104
|
+
return current === null || candidate < current ? candidate : current;
|
|
1105
|
+
}
|
|
1106
|
+
function later(current, candidate) {
|
|
1107
|
+
return current === null || candidate > current ? candidate : current;
|
|
1108
|
+
}
|
|
1109
|
+
function compareStrings(left, right) {
|
|
1110
|
+
if (left < right) {
|
|
1111
|
+
return -1;
|
|
1112
|
+
}
|
|
1113
|
+
return left > right ? 1 : 0;
|
|
1114
|
+
}
|
|
670
1115
|
// packages/core/src/builtins/complexity.ts
|
|
671
1116
|
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
672
1117
|
var COMPLEXITY_GATE_CONFIG = {
|
|
@@ -834,6 +1279,18 @@ function createFifoJevPort(port, config) {
|
|
|
834
1279
|
}
|
|
835
1280
|
}
|
|
836
1281
|
function start(pending) {
|
|
1282
|
+
if (pending.kind === "BATCH") {
|
|
1283
|
+
Promise.resolve().then(() => port.evaluateRuleBatch(pending.requests)).then((results) => {
|
|
1284
|
+
active -= 1;
|
|
1285
|
+
pending.settle(results);
|
|
1286
|
+
drain();
|
|
1287
|
+
}, (error) => {
|
|
1288
|
+
active -= 1;
|
|
1289
|
+
pending.fail(error);
|
|
1290
|
+
drain();
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
837
1294
|
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
838
1295
|
active -= 1;
|
|
839
1296
|
pending.settle(result);
|
|
@@ -847,7 +1304,13 @@ function createFifoJevPort(port, config) {
|
|
|
847
1304
|
return {
|
|
848
1305
|
evaluate(request) {
|
|
849
1306
|
return new Promise((settle, fail) => {
|
|
850
|
-
queue.push({ request, settle, fail });
|
|
1307
|
+
queue.push({ kind: "SINGLE", request, settle, fail });
|
|
1308
|
+
drain();
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
evaluateRuleBatch(requests) {
|
|
1312
|
+
return new Promise((settle, fail) => {
|
|
1313
|
+
queue.push({ kind: "BATCH", requests, settle, fail });
|
|
851
1314
|
drain();
|
|
852
1315
|
});
|
|
853
1316
|
}
|
|
@@ -910,7 +1373,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
|
910
1373
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
911
1374
|
autoPropose: true,
|
|
912
1375
|
proposeOn: ["FAIL"],
|
|
913
|
-
model:
|
|
1376
|
+
model: null
|
|
914
1377
|
};
|
|
915
1378
|
|
|
916
1379
|
// packages/core/src/remediation/request.ts
|
|
@@ -1000,6 +1463,7 @@ function snapshotRule(rule) {
|
|
|
1000
1463
|
// packages/core/src/remediation/config.ts
|
|
1001
1464
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
1002
1465
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
1466
|
+
var INVALID_MODEL = Symbol("invalid-model");
|
|
1003
1467
|
function resolveRemediationConfig(value) {
|
|
1004
1468
|
if (value === null || value === undefined) {
|
|
1005
1469
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
@@ -1022,8 +1486,8 @@ function resolveRemediationConfig(value) {
|
|
|
1022
1486
|
if (proposeOn === null) {
|
|
1023
1487
|
return invalid2();
|
|
1024
1488
|
}
|
|
1025
|
-
const model = remediation.model
|
|
1026
|
-
if (
|
|
1489
|
+
const model = resolveModel(remediation.model);
|
|
1490
|
+
if (model === INVALID_MODEL) {
|
|
1027
1491
|
return invalid2();
|
|
1028
1492
|
}
|
|
1029
1493
|
return valid({ autoPropose, proposeOn, model });
|
|
@@ -1038,6 +1502,15 @@ function parseModelSpecifier(model) {
|
|
|
1038
1502
|
modelID: model.slice(separator + 1)
|
|
1039
1503
|
};
|
|
1040
1504
|
}
|
|
1505
|
+
function resolveModel(value) {
|
|
1506
|
+
if (value === undefined) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
|
|
1510
|
+
return INVALID_MODEL;
|
|
1511
|
+
}
|
|
1512
|
+
return value;
|
|
1513
|
+
}
|
|
1041
1514
|
function readProposeOn(value) {
|
|
1042
1515
|
if (value === undefined) {
|
|
1043
1516
|
return DEFAULT_REMEDIATION_CONFIG.proposeOn;
|