@pablozrrrr/jevguard 0.2.2 → 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 +75 -16
- package/cli/main.js +951 -70
- package/index.js +877 -73
- package/package.json +1 -1
- package/skills/jev-init/SKILL.md +8 -2
- package/skills/jev-init/validate-init.js +528 -65
- package/skills/jevguard-rules/SKILL.md +44 -2
- package/skills/jevguard-rules/validate-rules.js +528 -65
package/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function buildReviewContext(turn, rule, scopedPaths) {
|
|
|
35
35
|
var DEFAULT_MAX_DIFF_LENGTH = 1e5;
|
|
36
36
|
var DEFAULT_EVIDENCE_POLICY = {
|
|
37
37
|
maxDiffLength: DEFAULT_MAX_DIFF_LENGTH,
|
|
38
|
-
|
|
38
|
+
codeExtensions: [
|
|
39
39
|
".ts",
|
|
40
40
|
".tsx",
|
|
41
41
|
".mts",
|
|
@@ -46,9 +46,6 @@ var DEFAULT_EVIDENCE_POLICY = {
|
|
|
46
46
|
".cjs",
|
|
47
47
|
".json",
|
|
48
48
|
".jsonc",
|
|
49
|
-
".md",
|
|
50
|
-
".mdx",
|
|
51
|
-
".txt",
|
|
52
49
|
".yaml",
|
|
53
50
|
".yml",
|
|
54
51
|
".toml",
|
|
@@ -104,6 +101,7 @@ var DEFAULT_EVIDENCE_POLICY = {
|
|
|
104
101
|
".diff",
|
|
105
102
|
".patch"
|
|
106
103
|
],
|
|
104
|
+
docsExtensions: [".md", ".mdx", ".txt"],
|
|
107
105
|
deniedFileNames: [
|
|
108
106
|
".env",
|
|
109
107
|
".envrc",
|
|
@@ -178,18 +176,26 @@ function checkFilePath(path, policy) {
|
|
|
178
176
|
if (policy.deniedExtensions.includes(extension)) {
|
|
179
177
|
return DENIED_SENSITIVE;
|
|
180
178
|
}
|
|
181
|
-
if (!
|
|
179
|
+
if (!isAllowlistedExtension(extension, policy)) {
|
|
182
180
|
return DENIED_EXTENSION;
|
|
183
181
|
}
|
|
184
182
|
return { allowed: true };
|
|
185
183
|
}
|
|
184
|
+
function isAllowlistedExtension(extension, policy) {
|
|
185
|
+
return policy.codeExtensions.includes(extension) || policy.docsExtensions.includes(extension);
|
|
186
|
+
}
|
|
186
187
|
function matchesDeniedFileName(basename, policy) {
|
|
187
188
|
return policy.deniedFileNames.some((name) => name.startsWith(".") ? basename === name || basename.startsWith(`${name}.`) : basename === name);
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
// packages/core/src/evidence/scope.ts
|
|
191
|
-
function
|
|
192
|
-
|
|
192
|
+
function matchesRuleScope(path, scope) {
|
|
193
|
+
const normalized = normalizePath(path);
|
|
194
|
+
const included = scope.include.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
195
|
+
if (!included) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return !scope.exclude.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
193
199
|
}
|
|
194
200
|
function globToRegExp(glob) {
|
|
195
201
|
const source = glob.replace(/\\/g, "/");
|
|
@@ -216,16 +222,6 @@ function globToRegExp(glob) {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
// packages/core/src/evidence/select-evidence.ts
|
|
219
|
-
function selectRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
220
|
-
if (hasNoAttributedPatch(turn)) {
|
|
221
|
-
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
222
|
-
}
|
|
223
|
-
const applicableFiles = selectApplicableFiles(turn.files, rule.scope);
|
|
224
|
-
if (applicableFiles.length === 0) {
|
|
225
|
-
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
226
|
-
}
|
|
227
|
-
return mountEvidence(applicableFiles, policy);
|
|
228
|
-
}
|
|
229
225
|
function selectTurnEvidence(turn, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
230
226
|
if (hasNoAttributedPatch(turn)) {
|
|
231
227
|
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
@@ -252,11 +248,21 @@ function mountEvidence(files, policy) {
|
|
|
252
248
|
function hasNoAttributedPatch(turn) {
|
|
253
249
|
return turn.files.every((file) => file.patch.trim() === "");
|
|
254
250
|
}
|
|
255
|
-
function selectApplicableFiles(files, scope) {
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
function selectApplicableFiles(files, scope, evidenceClass, policy) {
|
|
252
|
+
const scoped = scope === null ? files : files.filter((file) => matchesRuleScope(file.path, scope));
|
|
253
|
+
return scoped.filter((file) => isInEvidenceClass(file.path, evidenceClass, policy));
|
|
254
|
+
}
|
|
255
|
+
function isInEvidenceClass(path, evidenceClass, policy) {
|
|
256
|
+
if (evidenceClass === "any") {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
const safety = checkFilePath(path, policy);
|
|
260
|
+
if (!safety.allowed && safety.reason === "DENIED_SENSITIVE_PATH") {
|
|
261
|
+
return true;
|
|
258
262
|
}
|
|
259
|
-
|
|
263
|
+
const extension = fileExtension(path);
|
|
264
|
+
const otherClass = evidenceClass === "code" ? policy.docsExtensions : policy.codeExtensions;
|
|
265
|
+
return !otherClass.includes(extension);
|
|
260
266
|
}
|
|
261
267
|
function assembleDiff(files) {
|
|
262
268
|
return files.map((file) => file.patch).join(`
|
|
@@ -297,14 +303,20 @@ function evaluateWarningGate(probability, thresholds) {
|
|
|
297
303
|
}
|
|
298
304
|
|
|
299
305
|
// packages/core/src/evaluation/evaluate-with-port.ts
|
|
300
|
-
async function
|
|
306
|
+
async function evaluateRuleBatchWithPort(port, requests) {
|
|
301
307
|
try {
|
|
302
|
-
const
|
|
303
|
-
|
|
308
|
+
const results = await port.evaluateRuleBatch(requests);
|
|
309
|
+
if (results.length !== requests.length) {
|
|
310
|
+
return requests.map(() => ruleFailure("INVALID_RESPONSE"));
|
|
311
|
+
}
|
|
312
|
+
return results.map((result) => result.kind === "RULE" ? result : ruleFailure("INVALID_RESPONSE"));
|
|
304
313
|
} catch {
|
|
305
|
-
return
|
|
314
|
+
return requests.map(() => ruleFailure("API_ERROR"));
|
|
306
315
|
}
|
|
307
316
|
}
|
|
317
|
+
function ruleFailure(reason) {
|
|
318
|
+
return { kind: "RULE", status: "FAILED", reason };
|
|
319
|
+
}
|
|
308
320
|
async function evaluateBuiltInBatchWithPort(port, request) {
|
|
309
321
|
try {
|
|
310
322
|
const result = await port.evaluate(request);
|
|
@@ -313,27 +325,118 @@ async function evaluateBuiltInBatchWithPort(port, request) {
|
|
|
313
325
|
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "API_ERROR" };
|
|
314
326
|
}
|
|
315
327
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const context = buildReviewContext(input.turn, input.rule, []);
|
|
322
|
-
return { ...context, outcome: "SKIPPED", reason: selection.reason };
|
|
328
|
+
// packages/core/src/evidence/plan-slices.ts
|
|
329
|
+
var HUNK_MARKER = /^@@ [^\n]*@@/gm;
|
|
330
|
+
function planRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
331
|
+
if (hasNoAttributedPatch(turn)) {
|
|
332
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
323
333
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return {
|
|
334
|
+
const applicableFiles = selectApplicableFiles(turn.files, rule.scope, rule.evidence, policy);
|
|
335
|
+
if (applicableFiles.length === 0) {
|
|
336
|
+
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
327
337
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
338
|
+
const diff = assembleDiff(applicableFiles);
|
|
339
|
+
if (diff.trim() === "") {
|
|
340
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
341
|
+
}
|
|
342
|
+
if (applicableFiles.some((file) => !checkFilePath(file.path, policy).allowed)) {
|
|
343
|
+
return { status: "UNAVAILABLE", reason: "BLOCKED_EVIDENCE" };
|
|
344
|
+
}
|
|
345
|
+
if (diff.length <= policy.maxDiffLength) {
|
|
346
|
+
return { status: "PLANNED", slices: [wholeSlice(applicableFiles, diff)] };
|
|
333
347
|
}
|
|
334
|
-
|
|
335
|
-
|
|
348
|
+
return sliceByFile(applicableFiles, policy);
|
|
349
|
+
}
|
|
350
|
+
function wholeSlice(files, diff) {
|
|
351
|
+
const paths = files.map((file) => file.path);
|
|
352
|
+
return {
|
|
353
|
+
index: 0,
|
|
354
|
+
kind: "WHOLE",
|
|
355
|
+
path: paths.length === 1 ? paths[0] ?? null : null,
|
|
356
|
+
hunkOrdinal: null,
|
|
357
|
+
files: paths,
|
|
358
|
+
diff
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function sliceByFile(files, policy) {
|
|
362
|
+
const slices = [];
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
if (file.patch.length <= policy.maxDiffLength) {
|
|
365
|
+
slices.push(fileSlice(slices.length, file));
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const hunks = sliceHunks(slices.length, file, policy);
|
|
369
|
+
if (hunks === null) {
|
|
370
|
+
return { status: "UNAVAILABLE", reason: "OVERSIZED_DIFF" };
|
|
371
|
+
}
|
|
372
|
+
slices.push(...hunks);
|
|
373
|
+
}
|
|
374
|
+
return { status: "PLANNED", slices };
|
|
375
|
+
}
|
|
376
|
+
function fileSlice(index, file) {
|
|
377
|
+
return {
|
|
378
|
+
index,
|
|
379
|
+
kind: "FILE",
|
|
380
|
+
path: file.path,
|
|
381
|
+
hunkOrdinal: null,
|
|
382
|
+
files: [file.path],
|
|
383
|
+
diff: file.patch
|
|
384
|
+
};
|
|
336
385
|
}
|
|
386
|
+
function sliceHunks(startIndex, file, policy) {
|
|
387
|
+
const starts = hunkStarts(file.patch);
|
|
388
|
+
if (starts.length === 0) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const firstStart = starts[0];
|
|
392
|
+
if (firstStart === undefined) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const preamble = file.patch.slice(0, firstStart);
|
|
396
|
+
const slices = [];
|
|
397
|
+
for (let ordinal = 0;ordinal < starts.length; ordinal += 1) {
|
|
398
|
+
const start = starts[ordinal];
|
|
399
|
+
if (start === undefined) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const end = starts[ordinal + 1] ?? file.patch.length;
|
|
403
|
+
const rendered = preamble + file.patch.slice(start, end);
|
|
404
|
+
if (rendered.length > policy.maxDiffLength) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
slices.push({
|
|
408
|
+
index: startIndex + ordinal,
|
|
409
|
+
kind: "HUNK",
|
|
410
|
+
path: file.path,
|
|
411
|
+
hunkOrdinal: ordinal,
|
|
412
|
+
files: [file.path],
|
|
413
|
+
diff: rendered
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return slices;
|
|
417
|
+
}
|
|
418
|
+
function hunkStarts(patch) {
|
|
419
|
+
return Array.from(patch.matchAll(HUNK_MARKER), (match) => match.index ?? 0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// packages/core/src/evidence/slice-budget.ts
|
|
423
|
+
var MAX_SLICES_PER_RULE = 16;
|
|
424
|
+
var MAX_EXTRA_RULE_CALLS_PER_TURN = 32;
|
|
425
|
+
function planTurnRuleSlices(plans) {
|
|
426
|
+
let remainingExtraCalls = MAX_EXTRA_RULE_CALLS_PER_TURN;
|
|
427
|
+
return plans.map((plan) => {
|
|
428
|
+
if (plan.status !== "PLANNED") {
|
|
429
|
+
return plan;
|
|
430
|
+
}
|
|
431
|
+
const extraCalls = plan.slices.length - 1;
|
|
432
|
+
if (plan.slices.length > MAX_SLICES_PER_RULE || extraCalls > remainingExtraCalls) {
|
|
433
|
+
return { status: "SLICE_LIMIT_EXCEEDED", slices: plan.slices };
|
|
434
|
+
}
|
|
435
|
+
remainingExtraCalls -= extraCalls;
|
|
436
|
+
return plan;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
337
440
|
// packages/core/src/review/aggregate.ts
|
|
338
441
|
var VERDICT_RANK = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
339
442
|
function aggregateReview(turnId, results) {
|
|
@@ -387,28 +490,204 @@ function isSemanticVerdict(outcome) {
|
|
|
387
490
|
return outcome === "PASS" || outcome === "WARN" || outcome === "FAIL";
|
|
388
491
|
}
|
|
389
492
|
|
|
390
|
-
// packages/core/src/evaluation/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
493
|
+
// packages/core/src/evaluation/assemble-rule.ts
|
|
494
|
+
var VERDICT_RANK2 = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
495
|
+
function assembleSlicedRuleResult(rule, context, slices, evaluations, gateConfig) {
|
|
496
|
+
if (slices.length === 0) {
|
|
497
|
+
return { ...context, outcome: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
498
|
+
}
|
|
499
|
+
const summaries = slices.map(sliceSummary);
|
|
500
|
+
const mode = evidenceMode(slices.length);
|
|
501
|
+
const gates = [];
|
|
502
|
+
const judgments = [];
|
|
503
|
+
let failureIndex = null;
|
|
504
|
+
for (let index = 0;index < slices.length; index += 1) {
|
|
505
|
+
const slice = slices[index];
|
|
506
|
+
const evaluation = evaluations[index];
|
|
507
|
+
if (slice === undefined || evaluation === undefined || evaluation.status === "FAILED") {
|
|
508
|
+
failureIndex = index;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
const gate = evaluateGate(rule.severity, evaluation.noul.violationProbability, gateConfig);
|
|
512
|
+
if (gate.outcome === "UNAVAILABLE") {
|
|
513
|
+
failureIndex = index;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
gates.push(gate);
|
|
517
|
+
judgments.push({
|
|
518
|
+
index: slice.index,
|
|
519
|
+
probability: gate.violationProbability,
|
|
520
|
+
outcome: gate.outcome
|
|
521
|
+
});
|
|
395
522
|
}
|
|
396
|
-
|
|
523
|
+
if (failureIndex !== null) {
|
|
524
|
+
return {
|
|
525
|
+
...context,
|
|
526
|
+
outcome: "UNAVAILABLE",
|
|
527
|
+
reason: "JEV_FAILURE",
|
|
528
|
+
evidence: failedMetadata(mode, summaries, failureIndex)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
...context,
|
|
533
|
+
outcome: mergeVerdict(gates),
|
|
534
|
+
violationProbability: maxProbability(gates),
|
|
535
|
+
evidence: completeMetadata(mode, summaries, judgments)
|
|
536
|
+
};
|
|
397
537
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
538
|
+
function overBudgetMetadata(slices) {
|
|
539
|
+
const summaries = slices.map(sliceSummary);
|
|
540
|
+
return {
|
|
541
|
+
mode: "SLICED",
|
|
542
|
+
plannedSliceCount: summaries.length,
|
|
543
|
+
evaluatedSliceCount: 0,
|
|
544
|
+
failedSliceCount: 0,
|
|
545
|
+
notEvaluatedSliceCount: summaries.length,
|
|
546
|
+
slices: summaries,
|
|
547
|
+
sliceJudgments: [],
|
|
548
|
+
failingSliceIndex: null,
|
|
549
|
+
failureReason: "SLICE_LIMIT_EXCEEDED"
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function sliceSummary(slice) {
|
|
553
|
+
return {
|
|
554
|
+
index: slice.index,
|
|
555
|
+
path: slice.path,
|
|
556
|
+
kind: slice.kind,
|
|
557
|
+
hunkOrdinal: slice.hunkOrdinal
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function evidenceMode(sliceCount) {
|
|
561
|
+
return sliceCount === 1 ? "WHOLE" : "SLICED";
|
|
562
|
+
}
|
|
563
|
+
function mergeVerdict(gates) {
|
|
564
|
+
let verdict = "PASS";
|
|
565
|
+
for (const gate of gates) {
|
|
566
|
+
if (VERDICT_RANK2[gate.outcome] > VERDICT_RANK2[verdict]) {
|
|
567
|
+
verdict = gate.outcome;
|
|
568
|
+
}
|
|
401
569
|
}
|
|
570
|
+
return verdict;
|
|
571
|
+
}
|
|
572
|
+
function maxProbability(gates) {
|
|
573
|
+
return gates.reduce((highest, gate) => Math.max(highest, gate.violationProbability), Number.NEGATIVE_INFINITY);
|
|
574
|
+
}
|
|
575
|
+
function completeMetadata(mode, summaries, judgments) {
|
|
576
|
+
return {
|
|
577
|
+
mode,
|
|
578
|
+
plannedSliceCount: summaries.length,
|
|
579
|
+
evaluatedSliceCount: summaries.length,
|
|
580
|
+
failedSliceCount: 0,
|
|
581
|
+
notEvaluatedSliceCount: 0,
|
|
582
|
+
slices: summaries,
|
|
583
|
+
sliceJudgments: judgments,
|
|
584
|
+
failingSliceIndex: null,
|
|
585
|
+
failureReason: null
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function failedMetadata(mode, summaries, failingIndex) {
|
|
589
|
+
return {
|
|
590
|
+
mode,
|
|
591
|
+
plannedSliceCount: summaries.length,
|
|
592
|
+
evaluatedSliceCount: failingIndex,
|
|
593
|
+
failedSliceCount: 1,
|
|
594
|
+
notEvaluatedSliceCount: summaries.length - failingIndex - 1,
|
|
595
|
+
slices: summaries,
|
|
596
|
+
sliceJudgments: [],
|
|
597
|
+
failingSliceIndex: failingIndex,
|
|
598
|
+
failureReason: "JEV_FAILURE"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// packages/core/src/evaluation/evaluate-rules.ts
|
|
603
|
+
async function evaluateRules(input, dependencies) {
|
|
402
604
|
if (input.gateConfig.status === "INVALID") {
|
|
403
|
-
|
|
605
|
+
const results = input.rules.map((candidate) => candidate.status === "INVALID" ? invalidRuleResult(input.turn.id, candidate) : invalidConfigResult(input.turn, candidate.rule));
|
|
606
|
+
return aggregateReview(input.turn.id, results);
|
|
607
|
+
}
|
|
608
|
+
const gateConfig = input.gateConfig.config;
|
|
609
|
+
const slots = resolveSlots(input);
|
|
610
|
+
const requests = collectRequests(input.turn.task, slots);
|
|
611
|
+
const evaluations = requests.length === 0 ? [] : await evaluateRuleBatchWithPort(dependencies.jev, requests);
|
|
612
|
+
return aggregateReview(input.turn.id, assembleResults(input, slots, evaluations, gateConfig));
|
|
613
|
+
}
|
|
614
|
+
function resolveSlots(input) {
|
|
615
|
+
const planned = input.rules.map((candidate) => candidate.status === "INVALID" ? { kind: "RESULT", result: invalidRuleResult(input.turn.id, candidate) } : {
|
|
616
|
+
kind: "PLAN",
|
|
617
|
+
rule: candidate.rule,
|
|
618
|
+
plan: planRuleEvidence(input.turn, candidate.rule, input.evidencePolicy)
|
|
619
|
+
});
|
|
620
|
+
const budgeted = planTurnRuleSlices(planned.flatMap((slot) => slot.kind === "PLAN" ? [slot.plan] : []));
|
|
621
|
+
let cursor = 0;
|
|
622
|
+
return planned.map((slot) => {
|
|
623
|
+
if (slot.kind === "RESULT") {
|
|
624
|
+
return slot;
|
|
625
|
+
}
|
|
626
|
+
const plan = budgeted[cursor];
|
|
627
|
+
cursor += 1;
|
|
628
|
+
return plan === undefined ? slot : { kind: "PLAN", rule: slot.rule, plan };
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function collectRequests(task, slots) {
|
|
632
|
+
const requests = [];
|
|
633
|
+
for (const slot of slots) {
|
|
634
|
+
if (slot.kind !== "PLAN" || slot.plan.status !== "PLANNED") {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
for (const slice of slot.plan.slices) {
|
|
638
|
+
requests.push(buildJevRequest(task, slot.rule, { files: slice.files, diff: slice.diff }));
|
|
639
|
+
}
|
|
404
640
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
641
|
+
return requests;
|
|
642
|
+
}
|
|
643
|
+
function assembleResults(input, slots, evaluations, gateConfig) {
|
|
644
|
+
const results = [];
|
|
645
|
+
let evaluationCursor = 0;
|
|
646
|
+
for (const slot of slots) {
|
|
647
|
+
if (slot.kind === "RESULT") {
|
|
648
|
+
results.push(slot.result);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const plan = slot.plan;
|
|
652
|
+
if (plan.status === "SKIPPED") {
|
|
653
|
+
results.push(skippedResult(input.turn, slot.rule, plan.reason));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (plan.status === "UNAVAILABLE") {
|
|
657
|
+
results.push(unavailableResult(input.turn, slot.rule, plan.reason));
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (plan.status === "SLICE_LIMIT_EXCEEDED") {
|
|
661
|
+
results.push({
|
|
662
|
+
...buildReviewContext(input.turn, slot.rule, collectSlicePaths(plan.slices)),
|
|
663
|
+
outcome: "UNAVAILABLE",
|
|
664
|
+
reason: "SLICE_LIMIT_EXCEEDED",
|
|
665
|
+
evidence: overBudgetMetadata(plan.slices)
|
|
666
|
+
});
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const sliceEvaluations = evaluations.slice(evaluationCursor, evaluationCursor + plan.slices.length);
|
|
670
|
+
evaluationCursor += plan.slices.length;
|
|
671
|
+
results.push(assembleSlicedRuleResult(slot.rule, buildReviewContext(input.turn, slot.rule, collectSlicePaths(plan.slices)), plan.slices, sliceEvaluations, gateConfig));
|
|
672
|
+
}
|
|
673
|
+
return results;
|
|
674
|
+
}
|
|
675
|
+
function collectSlicePaths(slices) {
|
|
676
|
+
const paths = [];
|
|
677
|
+
for (const slice of slices) {
|
|
678
|
+
for (const path of slice.files) {
|
|
679
|
+
if (!paths.includes(path)) {
|
|
680
|
+
paths.push(path);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return paths;
|
|
685
|
+
}
|
|
686
|
+
function skippedResult(turn, rule, reason) {
|
|
687
|
+
return { ...buildReviewContext(turn, rule, []), outcome: "SKIPPED", reason };
|
|
688
|
+
}
|
|
689
|
+
function unavailableResult(turn, rule, reason) {
|
|
690
|
+
return { ...buildReviewContext(turn, rule, []), outcome: "UNAVAILABLE", reason };
|
|
412
691
|
}
|
|
413
692
|
function invalidRuleResult(turnId, failure) {
|
|
414
693
|
return {
|
|
@@ -450,13 +729,29 @@ function matchSectionHeading(line) {
|
|
|
450
729
|
var METADATA_ENTRY = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
451
730
|
function parseRuleMetadata(lines) {
|
|
452
731
|
let severity = null;
|
|
453
|
-
let
|
|
732
|
+
let evidence = null;
|
|
733
|
+
let scopeDeclared = false;
|
|
734
|
+
const includePatterns = [];
|
|
735
|
+
const excludePatterns = [];
|
|
454
736
|
const seenKeys = new Set;
|
|
455
737
|
for (const line of collectMetadataLines(lines)) {
|
|
456
738
|
const entry = parseMetadataEntry(line);
|
|
457
739
|
if (entry === null) {
|
|
458
740
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
459
741
|
}
|
|
742
|
+
if (entry.key === "scope") {
|
|
743
|
+
const pattern = parseScopePattern(entry.value);
|
|
744
|
+
if (pattern === null) {
|
|
745
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
746
|
+
}
|
|
747
|
+
scopeDeclared = true;
|
|
748
|
+
if (pattern.exclude) {
|
|
749
|
+
excludePatterns.push(pattern.pattern);
|
|
750
|
+
} else {
|
|
751
|
+
includePatterns.push(pattern.pattern);
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
460
755
|
if (seenKeys.has(entry.key)) {
|
|
461
756
|
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
462
757
|
}
|
|
@@ -469,11 +764,12 @@ function parseRuleMetadata(lines) {
|
|
|
469
764
|
severity = parsed;
|
|
470
765
|
continue;
|
|
471
766
|
}
|
|
472
|
-
if (entry.key === "
|
|
473
|
-
|
|
767
|
+
if (entry.key === "evidence") {
|
|
768
|
+
const parsed = parseEvidenceClass(entry.value);
|
|
769
|
+
if (parsed === null) {
|
|
474
770
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
475
771
|
}
|
|
476
|
-
|
|
772
|
+
evidence = parsed;
|
|
477
773
|
continue;
|
|
478
774
|
}
|
|
479
775
|
return { ok: false, code: "MALFORMED_METADATA" };
|
|
@@ -481,7 +777,11 @@ function parseRuleMetadata(lines) {
|
|
|
481
777
|
if (severity === null) {
|
|
482
778
|
return { ok: false, code: "MISSING_SEVERITY" };
|
|
483
779
|
}
|
|
484
|
-
|
|
780
|
+
if (scopeDeclared && includePatterns.length === 0) {
|
|
781
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
782
|
+
}
|
|
783
|
+
const scope = scopeDeclared ? { include: includePatterns, exclude: excludePatterns } : null;
|
|
784
|
+
return { ok: true, value: { severity, scope, evidence: evidence ?? "code" } };
|
|
485
785
|
}
|
|
486
786
|
function collectMetadataLines(lines) {
|
|
487
787
|
const collected = [];
|
|
@@ -511,6 +811,26 @@ function parseSeverity(value) {
|
|
|
511
811
|
return null;
|
|
512
812
|
}
|
|
513
813
|
}
|
|
814
|
+
function parseEvidenceClass(value) {
|
|
815
|
+
switch (value) {
|
|
816
|
+
case "code":
|
|
817
|
+
case "docs":
|
|
818
|
+
case "any":
|
|
819
|
+
return value;
|
|
820
|
+
default:
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function parseScopePattern(value) {
|
|
825
|
+
if (value === "") {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
if (value.startsWith("!")) {
|
|
829
|
+
const pattern = value.slice(1).trim();
|
|
830
|
+
return pattern === "" ? null : { pattern, exclude: true };
|
|
831
|
+
}
|
|
832
|
+
return { pattern: value, exclude: false };
|
|
833
|
+
}
|
|
514
834
|
|
|
515
835
|
// packages/core/src/rules/sections.ts
|
|
516
836
|
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
@@ -633,6 +953,7 @@ function buildParsedRule(id, metadata, sections) {
|
|
|
633
953
|
id,
|
|
634
954
|
severity: metadata.severity,
|
|
635
955
|
scope: metadata.scope,
|
|
956
|
+
evidence: metadata.evidence,
|
|
636
957
|
description: sections.rule,
|
|
637
958
|
violation: sections.violation,
|
|
638
959
|
allowed: sections.allowed
|
|
@@ -663,6 +984,130 @@ function findDuplicateIds(ids) {
|
|
|
663
984
|
}
|
|
664
985
|
return duplicates;
|
|
665
986
|
}
|
|
987
|
+
// packages/core/src/report/aggregate.ts
|
|
988
|
+
function aggregateReviewHistory(records, skippedLineCount) {
|
|
989
|
+
const outcomes = emptyAccumulator();
|
|
990
|
+
const unavailableReasons = new Map;
|
|
991
|
+
const skippedReasons = new Map;
|
|
992
|
+
const builtIns = new Map;
|
|
993
|
+
const rules = new Map;
|
|
994
|
+
const evaluation = { wholeEvaluations: 0, slicedEvaluations: 0, totalSliceJudgments: 0 };
|
|
995
|
+
let firstTimestamp = null;
|
|
996
|
+
let lastTimestamp = null;
|
|
997
|
+
for (const record of records) {
|
|
998
|
+
firstTimestamp = earlier(firstTimestamp, record.timestamp);
|
|
999
|
+
lastTimestamp = later(lastTimestamp, record.timestamp);
|
|
1000
|
+
for (const result of record.results) {
|
|
1001
|
+
countOutcome(outcomes, result.outcome);
|
|
1002
|
+
if (result.outcome === "UNAVAILABLE") {
|
|
1003
|
+
bump(unavailableReasons, result.reason);
|
|
1004
|
+
}
|
|
1005
|
+
if (result.outcome === "SKIPPED") {
|
|
1006
|
+
bump(skippedReasons, result.reason);
|
|
1007
|
+
}
|
|
1008
|
+
if (result.kind === "BUILT_IN") {
|
|
1009
|
+
bump(builtIns, result.checkId);
|
|
1010
|
+
}
|
|
1011
|
+
if (result.kind === "RULE" && result.ruleId !== null) {
|
|
1012
|
+
countRule(rules, result.ruleId, result.outcome, result.evidence);
|
|
1013
|
+
}
|
|
1014
|
+
if (result.kind === "RULE" && result.evidence !== undefined) {
|
|
1015
|
+
countEvidence(evaluation, result.evidence.mode, result.evidence.sliceJudgments.length);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
entryCount: records.length,
|
|
1021
|
+
skippedLineCount,
|
|
1022
|
+
firstTimestamp,
|
|
1023
|
+
lastTimestamp,
|
|
1024
|
+
outcomes: finalizeAccumulator(outcomes),
|
|
1025
|
+
unavailableReasons: sortedReasons(unavailableReasons),
|
|
1026
|
+
skippedReasons: sortedReasons(skippedReasons),
|
|
1027
|
+
builtIns: sortedBuiltIns(builtIns),
|
|
1028
|
+
rules: sortedRules(rules),
|
|
1029
|
+
evaluation: finalizeEvaluation(evaluation)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function emptyAccumulator() {
|
|
1033
|
+
return { pass: 0, warn: 0, fail: 0, unavailable: 0, skipped: 0 };
|
|
1034
|
+
}
|
|
1035
|
+
function countOutcome(counts, outcome) {
|
|
1036
|
+
switch (outcome) {
|
|
1037
|
+
case "PASS":
|
|
1038
|
+
counts.pass += 1;
|
|
1039
|
+
return;
|
|
1040
|
+
case "WARN":
|
|
1041
|
+
counts.warn += 1;
|
|
1042
|
+
return;
|
|
1043
|
+
case "FAIL":
|
|
1044
|
+
counts.fail += 1;
|
|
1045
|
+
return;
|
|
1046
|
+
case "UNAVAILABLE":
|
|
1047
|
+
counts.unavailable += 1;
|
|
1048
|
+
return;
|
|
1049
|
+
case "SKIPPED":
|
|
1050
|
+
counts.skipped += 1;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function countRule(rules, ruleId, outcome, evidence) {
|
|
1055
|
+
const accumulator = rules.get(ruleId) ?? { counts: emptyAccumulator(), sliced: 0 };
|
|
1056
|
+
countOutcome(accumulator.counts, outcome);
|
|
1057
|
+
if (evidence?.mode === "SLICED") {
|
|
1058
|
+
accumulator.sliced += 1;
|
|
1059
|
+
}
|
|
1060
|
+
rules.set(ruleId, accumulator);
|
|
1061
|
+
}
|
|
1062
|
+
function countEvidence(coverage, mode, sliceJudgmentCount) {
|
|
1063
|
+
if (mode === "WHOLE") {
|
|
1064
|
+
coverage.wholeEvaluations += 1;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
coverage.slicedEvaluations += 1;
|
|
1068
|
+
coverage.totalSliceJudgments += sliceJudgmentCount;
|
|
1069
|
+
}
|
|
1070
|
+
function finalizeAccumulator(counts) {
|
|
1071
|
+
return {
|
|
1072
|
+
pass: counts.pass,
|
|
1073
|
+
warn: counts.warn,
|
|
1074
|
+
fail: counts.fail,
|
|
1075
|
+
unavailable: counts.unavailable,
|
|
1076
|
+
skipped: counts.skipped,
|
|
1077
|
+
total: counts.pass + counts.warn + counts.fail + counts.unavailable + counts.skipped
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function finalizeEvaluation(coverage) {
|
|
1081
|
+
return { ...coverage };
|
|
1082
|
+
}
|
|
1083
|
+
function bump(map, key) {
|
|
1084
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
1085
|
+
}
|
|
1086
|
+
function sortedReasons(map) {
|
|
1087
|
+
return [...map.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareStrings(left.reason, right.reason));
|
|
1088
|
+
}
|
|
1089
|
+
function sortedBuiltIns(map) {
|
|
1090
|
+
return [...map.entries()].map(([checkId, count]) => ({ checkId, count })).sort((left, right) => compareStrings(left.checkId, right.checkId));
|
|
1091
|
+
}
|
|
1092
|
+
function sortedRules(map) {
|
|
1093
|
+
return [...map.entries()].map(([ruleId, accumulator]) => ({
|
|
1094
|
+
ruleId,
|
|
1095
|
+
counts: finalizeAccumulator(accumulator.counts),
|
|
1096
|
+
slicedRuleCount: accumulator.sliced
|
|
1097
|
+
})).sort((left, right) => compareStrings(left.ruleId, right.ruleId));
|
|
1098
|
+
}
|
|
1099
|
+
function earlier(current, candidate) {
|
|
1100
|
+
return current === null || candidate < current ? candidate : current;
|
|
1101
|
+
}
|
|
1102
|
+
function later(current, candidate) {
|
|
1103
|
+
return current === null || candidate > current ? candidate : current;
|
|
1104
|
+
}
|
|
1105
|
+
function compareStrings(left, right) {
|
|
1106
|
+
if (left < right) {
|
|
1107
|
+
return -1;
|
|
1108
|
+
}
|
|
1109
|
+
return left > right ? 1 : 0;
|
|
1110
|
+
}
|
|
666
1111
|
// packages/core/src/builtins/complexity.ts
|
|
667
1112
|
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
668
1113
|
var COMPLEXITY_GATE_CONFIG = {
|
|
@@ -830,6 +1275,18 @@ function createFifoJevPort(port, config) {
|
|
|
830
1275
|
}
|
|
831
1276
|
}
|
|
832
1277
|
function start(pending) {
|
|
1278
|
+
if (pending.kind === "BATCH") {
|
|
1279
|
+
Promise.resolve().then(() => port.evaluateRuleBatch(pending.requests)).then((results) => {
|
|
1280
|
+
active -= 1;
|
|
1281
|
+
pending.settle(results);
|
|
1282
|
+
drain();
|
|
1283
|
+
}, (error) => {
|
|
1284
|
+
active -= 1;
|
|
1285
|
+
pending.fail(error);
|
|
1286
|
+
drain();
|
|
1287
|
+
});
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
833
1290
|
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
834
1291
|
active -= 1;
|
|
835
1292
|
pending.settle(result);
|
|
@@ -843,7 +1300,13 @@ function createFifoJevPort(port, config) {
|
|
|
843
1300
|
return {
|
|
844
1301
|
evaluate(request) {
|
|
845
1302
|
return new Promise((settle, fail) => {
|
|
846
|
-
queue.push({ request, settle, fail });
|
|
1303
|
+
queue.push({ kind: "SINGLE", request, settle, fail });
|
|
1304
|
+
drain();
|
|
1305
|
+
});
|
|
1306
|
+
},
|
|
1307
|
+
evaluateRuleBatch(requests) {
|
|
1308
|
+
return new Promise((settle, fail) => {
|
|
1309
|
+
queue.push({ kind: "BATCH", requests, settle, fail });
|
|
847
1310
|
drain();
|
|
848
1311
|
});
|
|
849
1312
|
}
|
|
@@ -1520,6 +1983,7 @@ var JEV_MODEL = "jev-latest";
|
|
|
1520
1983
|
|
|
1521
1984
|
// packages/opencode-adapter/src/jev/transport.ts
|
|
1522
1985
|
var DEFAULT_ALLOWED_CRITERION = "The change does not violate the rule's Violation.";
|
|
1986
|
+
var RULE_BATCH_CONCURRENCY = 4;
|
|
1523
1987
|
function buildTypeSafeClientConfig(apiKey) {
|
|
1524
1988
|
return { apiKey, defaultModel: JEV_MODEL, logLevel: "off" };
|
|
1525
1989
|
}
|
|
@@ -1537,9 +2001,48 @@ function createJevTransport(dependencies) {
|
|
|
1537
2001
|
return failedFor(request, "API_ERROR");
|
|
1538
2002
|
}
|
|
1539
2003
|
return evaluateWithClient(client, request);
|
|
2004
|
+
},
|
|
2005
|
+
async evaluateRuleBatch(requests) {
|
|
2006
|
+
if (requests.length === 0) {
|
|
2007
|
+
return [];
|
|
2008
|
+
}
|
|
2009
|
+
const credential = await resolveCredential(dependencies.credentials);
|
|
2010
|
+
if (credential.status === "UNAVAILABLE") {
|
|
2011
|
+
const reason = toFailureReason(credential.reason);
|
|
2012
|
+
return requests.map(() => ruleFailed(reason));
|
|
2013
|
+
}
|
|
2014
|
+
let client;
|
|
2015
|
+
try {
|
|
2016
|
+
client = dependencies.createClient(credential.apiKey);
|
|
2017
|
+
} catch {
|
|
2018
|
+
return requests.map(() => ruleFailed("API_ERROR"));
|
|
2019
|
+
}
|
|
2020
|
+
return runWithConcurrency(requests, RULE_BATCH_CONCURRENCY, (request) => evaluateRuleWithClient(client, request));
|
|
1540
2021
|
}
|
|
1541
2022
|
};
|
|
1542
2023
|
}
|
|
2024
|
+
async function runWithConcurrency(items, cap, worker) {
|
|
2025
|
+
const results = new Array(items.length);
|
|
2026
|
+
let cursor = 0;
|
|
2027
|
+
async function run() {
|
|
2028
|
+
while (true) {
|
|
2029
|
+
const index = cursor;
|
|
2030
|
+
cursor += 1;
|
|
2031
|
+
if (index >= items.length) {
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const item = items[index];
|
|
2035
|
+
if (item === undefined) {
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
results[index] = await worker(item);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const workerCount = Math.min(cap, items.length);
|
|
2042
|
+
const workers = Array.from({ length: workerCount }, () => run());
|
|
2043
|
+
await Promise.all(workers);
|
|
2044
|
+
return results;
|
|
2045
|
+
}
|
|
1543
2046
|
function createTypeSafeJevTransport(credentials) {
|
|
1544
2047
|
return createJevTransport({ credentials, createClient: createTypeSafeSystemOneClient });
|
|
1545
2048
|
}
|
|
@@ -1988,6 +2491,261 @@ async function readOptionalFile2(fileSystem, path) {
|
|
|
1988
2491
|
function isMissingFileError2(error) {
|
|
1989
2492
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1990
2493
|
}
|
|
2494
|
+
// packages/opencode-adapter/src/history/local.ts
|
|
2495
|
+
import { isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
2496
|
+
var DATA_DIRECTORY = "jevguard";
|
|
2497
|
+
var HISTORY_FILENAME = "reviews.jsonl";
|
|
2498
|
+
var DEFAULT_HISTORY_MAX_BYTES = 5 * 1024 * 1024;
|
|
2499
|
+
var DEFAULT_HISTORY_MAX_RECORDS = 2000;
|
|
2500
|
+
function createLocalReviewHistory(dependencies) {
|
|
2501
|
+
const filePath = resolveHistoryFilePath(dependencies.context);
|
|
2502
|
+
const maxBytes = dependencies.maxBytes ?? DEFAULT_HISTORY_MAX_BYTES;
|
|
2503
|
+
const maxRecords = dependencies.maxRecords ?? DEFAULT_HISTORY_MAX_RECORDS;
|
|
2504
|
+
let tail = Promise.resolve();
|
|
2505
|
+
async function append(entry) {
|
|
2506
|
+
try {
|
|
2507
|
+
const record = {
|
|
2508
|
+
...entry,
|
|
2509
|
+
timestamp: dependencies.now().toISOString()
|
|
2510
|
+
};
|
|
2511
|
+
await dependencies.fileSystem.ensureParentDirectory(filePath);
|
|
2512
|
+
await dependencies.fileSystem.appendFile(filePath, `${JSON.stringify(record)}
|
|
2513
|
+
`);
|
|
2514
|
+
await boundGrowth();
|
|
2515
|
+
} catch {
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
async function boundGrowth() {
|
|
2520
|
+
try {
|
|
2521
|
+
const size = await dependencies.fileSystem.sizeOf(filePath);
|
|
2522
|
+
if (size === null || size <= maxBytes) {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
const contents = await dependencies.fileSystem.readFile(filePath);
|
|
2526
|
+
if (contents === null) {
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
const lines = contents.split(`
|
|
2530
|
+
`).filter((line) => line.trim() !== "");
|
|
2531
|
+
const kept = lines.slice(Math.max(0, lines.length - maxRecords));
|
|
2532
|
+
const rewritten = kept.length === 0 ? "" : `${kept.join(`
|
|
2533
|
+
`)}
|
|
2534
|
+
`;
|
|
2535
|
+
await dependencies.fileSystem.writeFileAtomic(filePath, rewritten);
|
|
2536
|
+
} catch {
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
async function read() {
|
|
2541
|
+
try {
|
|
2542
|
+
const contents = await dependencies.fileSystem.readFile(filePath);
|
|
2543
|
+
if (contents === null) {
|
|
2544
|
+
return { records: [], skippedLineCount: 0 };
|
|
2545
|
+
}
|
|
2546
|
+
return parseRecords(contents);
|
|
2547
|
+
} catch {
|
|
2548
|
+
return { records: [], skippedLineCount: 0 };
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
append: (entry) => {
|
|
2553
|
+
const run = tail.then(() => append(entry));
|
|
2554
|
+
tail = run.then(ignore, ignore);
|
|
2555
|
+
return run;
|
|
2556
|
+
},
|
|
2557
|
+
read
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
function resolveHistoryFilePath(context) {
|
|
2561
|
+
return join5(resolveDataHome(context), DATA_DIRECTORY, HISTORY_FILENAME);
|
|
2562
|
+
}
|
|
2563
|
+
function resolveDataHome(context) {
|
|
2564
|
+
const configured = context.environment.get("XDG_DATA_HOME");
|
|
2565
|
+
if (configured !== undefined && configured !== "" && isAbsolute2(configured)) {
|
|
2566
|
+
return configured;
|
|
2567
|
+
}
|
|
2568
|
+
return join5(context.homedir, ".local", "share");
|
|
2569
|
+
}
|
|
2570
|
+
function parseRecords(contents) {
|
|
2571
|
+
const records = [];
|
|
2572
|
+
let skippedLineCount = 0;
|
|
2573
|
+
for (const line of contents.split(`
|
|
2574
|
+
`)) {
|
|
2575
|
+
if (line.trim() === "") {
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
const parsed = parseRecord(line);
|
|
2579
|
+
if (parsed === null) {
|
|
2580
|
+
skippedLineCount += 1;
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
records.push(parsed);
|
|
2584
|
+
}
|
|
2585
|
+
return { records, skippedLineCount };
|
|
2586
|
+
}
|
|
2587
|
+
function parseRecord(line) {
|
|
2588
|
+
try {
|
|
2589
|
+
const value = JSON.parse(line);
|
|
2590
|
+
return isReviewHistoryRecord(value) ? value : null;
|
|
2591
|
+
} catch {
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function isReviewHistoryRecord(value) {
|
|
2596
|
+
if (!isObject(value)) {
|
|
2597
|
+
return false;
|
|
2598
|
+
}
|
|
2599
|
+
if (typeof value.timestamp !== "string" || typeof value.turnId !== "string") {
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
if (!isObject(value.summary) || !Array.isArray(value.results)) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
return value.results.every(isHistoryResult);
|
|
2606
|
+
}
|
|
2607
|
+
function isHistoryResult(value) {
|
|
2608
|
+
if (!isObject(value)) {
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2611
|
+
if (value.kind !== "RULE" && value.kind !== "BUILT_IN" && value.kind !== "REVIEW") {
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
if (!isReviewOutcome(value.outcome)) {
|
|
2615
|
+
return false;
|
|
2616
|
+
}
|
|
2617
|
+
if (value.outcome === "UNAVAILABLE" || value.outcome === "SKIPPED") {
|
|
2618
|
+
return typeof value.reason === "string" && hasValidEvidence(value);
|
|
2619
|
+
}
|
|
2620
|
+
if (typeof value.violationProbability !== "number") {
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
if (value.kind === "BUILT_IN") {
|
|
2624
|
+
return typeof value.checkId === "string" && hasValidEvidence(value);
|
|
2625
|
+
}
|
|
2626
|
+
if (value.kind === "RULE") {
|
|
2627
|
+
return (value.ruleId === null || typeof value.ruleId === "string") && hasValidEvidence(value);
|
|
2628
|
+
}
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
function hasValidEvidence(value) {
|
|
2632
|
+
const evidence = value.evidence;
|
|
2633
|
+
if (evidence === undefined) {
|
|
2634
|
+
return true;
|
|
2635
|
+
}
|
|
2636
|
+
if (!isObject(evidence)) {
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
if (evidence.mode !== "WHOLE" && evidence.mode !== "SLICED") {
|
|
2640
|
+
return false;
|
|
2641
|
+
}
|
|
2642
|
+
return Array.isArray(evidence.sliceJudgments);
|
|
2643
|
+
}
|
|
2644
|
+
function isReviewOutcome(value) {
|
|
2645
|
+
return value === "PASS" || value === "WARN" || value === "FAIL" || value === "UNAVAILABLE" || value === "SKIPPED";
|
|
2646
|
+
}
|
|
2647
|
+
function isObject(value) {
|
|
2648
|
+
return typeof value === "object" && value !== null;
|
|
2649
|
+
}
|
|
2650
|
+
function ignore() {
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
// packages/opencode-adapter/src/history/mirrored-sink.ts
|
|
2654
|
+
function createMirroredReviewLogSink(hostSink, history) {
|
|
2655
|
+
return {
|
|
2656
|
+
async write(entry) {
|
|
2657
|
+
const [hostStatus] = await Promise.all([
|
|
2658
|
+
writeHost(hostSink, entry),
|
|
2659
|
+
appendHistory(history, entry)
|
|
2660
|
+
]);
|
|
2661
|
+
return hostStatus;
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
async function writeHost(sink, entry) {
|
|
2666
|
+
try {
|
|
2667
|
+
return await sink.write(entry);
|
|
2668
|
+
} catch {
|
|
2669
|
+
return "FAILED";
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
async function appendHistory(history, entry) {
|
|
2673
|
+
try {
|
|
2674
|
+
await history.append(entry);
|
|
2675
|
+
} catch {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
// packages/opencode-adapter/src/history/node-file-system.ts
|
|
2680
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2681
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
2682
|
+
import { basename, dirname as dirname3, join as join6 } from "path";
|
|
2683
|
+
function createNodeReviewHistoryFileSystem() {
|
|
2684
|
+
return {
|
|
2685
|
+
readFile: readOptionalFile3,
|
|
2686
|
+
appendFile: appendHistoryFile,
|
|
2687
|
+
sizeOf: sizeOfOptionalFile,
|
|
2688
|
+
writeFileAtomic,
|
|
2689
|
+
ensureParentDirectory: async (path) => {
|
|
2690
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
async function readOptionalFile3(path) {
|
|
2695
|
+
try {
|
|
2696
|
+
return await readFile3(path, "utf8");
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
if (isMissingFileError3(error)) {
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
throw error;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async function appendHistoryFile(path, data) {
|
|
2705
|
+
await appendFile(path, data, "utf8");
|
|
2706
|
+
}
|
|
2707
|
+
async function sizeOfOptionalFile(path) {
|
|
2708
|
+
try {
|
|
2709
|
+
const info = await stat(path);
|
|
2710
|
+
return info.size;
|
|
2711
|
+
} catch (error) {
|
|
2712
|
+
if (isMissingFileError3(error)) {
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
throw error;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
async function writeFileAtomic(path, data) {
|
|
2719
|
+
const temporaryPath = join6(dirname3(path), `.${basename(path)}.jevguard-tmp-${randomUUID2()}`);
|
|
2720
|
+
try {
|
|
2721
|
+
await writeFile2(temporaryPath, data, "utf8");
|
|
2722
|
+
await rename2(temporaryPath, path);
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
await rm2(temporaryPath, { force: true }).catch(() => {
|
|
2725
|
+
return;
|
|
2726
|
+
});
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function isMissingFileError3(error) {
|
|
2731
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2732
|
+
}
|
|
2733
|
+
// packages/opencode-adapter/src/history/process-context.ts
|
|
2734
|
+
import { homedir } from "os";
|
|
2735
|
+
function createProcessReviewHistoryContext() {
|
|
2736
|
+
return {
|
|
2737
|
+
cwd: process.cwd(),
|
|
2738
|
+
environment: processEnvironment(),
|
|
2739
|
+
homedir: homedir()
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function processEnvironment() {
|
|
2743
|
+
return {
|
|
2744
|
+
get(name) {
|
|
2745
|
+
return process.env[name];
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
1991
2749
|
// packages/opencode-adapter/src/presentation/display.ts
|
|
1992
2750
|
function displayOutcome(counts) {
|
|
1993
2751
|
if (counts.fail > 0) {
|
|
@@ -2042,12 +2800,35 @@ function logLevelFor(outcome) {
|
|
|
2042
2800
|
function reviewLogMessage(entry) {
|
|
2043
2801
|
return `JevGuard ${displayOutcome(entry.summary.counts)} for turn ${entry.turnId}`;
|
|
2044
2802
|
}
|
|
2803
|
+
function projectEvidence(evidence) {
|
|
2804
|
+
return {
|
|
2805
|
+
mode: evidence.mode,
|
|
2806
|
+
plannedSliceCount: evidence.plannedSliceCount,
|
|
2807
|
+
evaluatedSliceCount: evidence.evaluatedSliceCount,
|
|
2808
|
+
failedSliceCount: evidence.failedSliceCount,
|
|
2809
|
+
notEvaluatedSliceCount: evidence.notEvaluatedSliceCount,
|
|
2810
|
+
slices: evidence.slices.map((slice) => ({
|
|
2811
|
+
index: slice.index,
|
|
2812
|
+
path: slice.path,
|
|
2813
|
+
kind: slice.kind,
|
|
2814
|
+
hunkOrdinal: slice.hunkOrdinal
|
|
2815
|
+
})),
|
|
2816
|
+
sliceJudgments: evidence.sliceJudgments.map((judgment) => ({
|
|
2817
|
+
index: judgment.index,
|
|
2818
|
+
probability: judgment.probability,
|
|
2819
|
+
outcome: judgment.outcome
|
|
2820
|
+
})),
|
|
2821
|
+
failingSliceIndex: evidence.failingSliceIndex,
|
|
2822
|
+
failureReason: evidence.failureReason
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2045
2825
|
function ruleLogResult(result) {
|
|
2046
2826
|
const identity = {
|
|
2047
2827
|
kind: "RULE",
|
|
2048
2828
|
ruleId: result.ruleId,
|
|
2049
2829
|
severity: result.severity,
|
|
2050
|
-
scopedPaths: [...result.scopedPaths]
|
|
2830
|
+
scopedPaths: [...result.scopedPaths],
|
|
2831
|
+
...result.evidence === undefined ? {} : { evidence: projectEvidence(result.evidence) }
|
|
2051
2832
|
};
|
|
2052
2833
|
switch (result.outcome) {
|
|
2053
2834
|
case "PASS":
|
|
@@ -2093,10 +2874,28 @@ function toReviewToast(review) {
|
|
|
2093
2874
|
const outcome = displayOutcome(review.summary.counts);
|
|
2094
2875
|
return {
|
|
2095
2876
|
title: `JevGuard ${outcome}`,
|
|
2096
|
-
message: countSummary(review.summary.counts)
|
|
2877
|
+
message: `${countSummary(review.summary.counts)}${slicedSummary(review.results)}`,
|
|
2097
2878
|
variant: toastVariantFor(outcome)
|
|
2098
2879
|
};
|
|
2099
2880
|
}
|
|
2881
|
+
function slicedSummary(results) {
|
|
2882
|
+
let slicedRules = 0;
|
|
2883
|
+
let sliceJudgments = 0;
|
|
2884
|
+
for (const result of results) {
|
|
2885
|
+
if (result.kind !== "RULE" || result.evidence === undefined) {
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
if (result.evidence.mode !== "SLICED") {
|
|
2889
|
+
continue;
|
|
2890
|
+
}
|
|
2891
|
+
slicedRules += 1;
|
|
2892
|
+
sliceJudgments += result.evidence.evaluatedSliceCount;
|
|
2893
|
+
}
|
|
2894
|
+
if (slicedRules === 0 && sliceJudgments === 0) {
|
|
2895
|
+
return "";
|
|
2896
|
+
}
|
|
2897
|
+
return ` \xB7 sliced ${slicedRules} rules \xB7 slices ${sliceJudgments}`;
|
|
2898
|
+
}
|
|
2100
2899
|
function toastVariantFor(outcome) {
|
|
2101
2900
|
switch (outcome) {
|
|
2102
2901
|
case "PASS":
|
|
@@ -2580,7 +3379,7 @@ function createPluginRuntime(dependencies) {
|
|
|
2580
3379
|
let attributionTail = Promise.resolve();
|
|
2581
3380
|
const backgroundJobs = new Set;
|
|
2582
3381
|
function track(work) {
|
|
2583
|
-
const contained = work.then(
|
|
3382
|
+
const contained = work.then(ignore2, ignore2);
|
|
2584
3383
|
backgroundJobs.add(contained);
|
|
2585
3384
|
contained.then(() => {
|
|
2586
3385
|
backgroundJobs.delete(contained);
|
|
@@ -2588,7 +3387,7 @@ function createPluginRuntime(dependencies) {
|
|
|
2588
3387
|
}
|
|
2589
3388
|
function enqueueAttribution(sessionID) {
|
|
2590
3389
|
const run = attributionTail.then(() => processIdle(sessionID));
|
|
2591
|
-
attributionTail = run.then(
|
|
3390
|
+
attributionTail = run.then(ignore2, ignore2);
|
|
2592
3391
|
}
|
|
2593
3392
|
async function processIdle(sessionID) {
|
|
2594
3393
|
try {
|
|
@@ -2637,7 +3436,7 @@ function createPluginRuntime(dependencies) {
|
|
|
2637
3436
|
drain
|
|
2638
3437
|
};
|
|
2639
3438
|
}
|
|
2640
|
-
function
|
|
3439
|
+
function ignore2() {
|
|
2641
3440
|
return;
|
|
2642
3441
|
}
|
|
2643
3442
|
|
|
@@ -2689,7 +3488,7 @@ function createConfigHook(options) {
|
|
|
2689
3488
|
|
|
2690
3489
|
// packages/plugin/src/skills/skill-directory.ts
|
|
2691
3490
|
import { existsSync } from "fs";
|
|
2692
|
-
import { join as
|
|
3491
|
+
import { join as join7 } from "path";
|
|
2693
3492
|
import { fileURLToPath } from "url";
|
|
2694
3493
|
var BUNDLED_SKILL_NAMES = ["jevguard-rules", "jev-init"];
|
|
2695
3494
|
var SKILL_ENTRY_FILE = "SKILL.md";
|
|
@@ -2697,7 +3496,7 @@ var SKILL_DIRECTORY_PREFIXES = ["./skills/", "../skills/"];
|
|
|
2697
3496
|
function resolveSkillDirectory(moduleUrl, skillName, exists) {
|
|
2698
3497
|
for (const prefix of SKILL_DIRECTORY_PREFIXES) {
|
|
2699
3498
|
const directory = fileURLToPath(new URL(`${prefix}${skillName}`, moduleUrl));
|
|
2700
|
-
if (exists(
|
|
3499
|
+
if (exists(join7(directory, SKILL_ENTRY_FILE))) {
|
|
2701
3500
|
return directory;
|
|
2702
3501
|
}
|
|
2703
3502
|
}
|
|
@@ -2719,6 +3518,11 @@ var DEFAULT_JEV_MAX_CONCURRENCY = 2;
|
|
|
2719
3518
|
var JevGuardPlugin = async (input) => {
|
|
2720
3519
|
const presentationClient = createOpenCodePresentationClient(input.client);
|
|
2721
3520
|
const toast = createOpenCodeToastSink(presentationClient);
|
|
3521
|
+
const history = createLocalReviewHistory({
|
|
3522
|
+
context: createProcessReviewHistoryContext(),
|
|
3523
|
+
fileSystem: createNodeReviewHistoryFileSystem(),
|
|
3524
|
+
now: () => new Date
|
|
3525
|
+
});
|
|
2722
3526
|
const dependencies = {
|
|
2723
3527
|
facade: createOpenCodeSessionFacade(input.client),
|
|
2724
3528
|
deduplicator: new InMemoryTurnDeduplicator,
|
|
@@ -2727,7 +3531,7 @@ var JevGuardPlugin = async (input) => {
|
|
|
2727
3531
|
fileSystem: createNodePolicyFileSystem()
|
|
2728
3532
|
}),
|
|
2729
3533
|
presenter: createReviewPresenter({
|
|
2730
|
-
log: createOpenCodeLogSink(presentationClient),
|
|
3534
|
+
log: createMirroredReviewLogSink(createOpenCodeLogSink(presentationClient), history),
|
|
2731
3535
|
toast
|
|
2732
3536
|
}),
|
|
2733
3537
|
proposals: createRemediationProposalStore(),
|