@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/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
- allowedExtensions: [
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 (!policy.allowedExtensions.includes(extension)) {
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 matchesScope(path, scope) {
192
- return globToRegExp(scope).test(normalizePath(path));
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
- if (scope === null) {
257
- return files;
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
- return files.filter((file) => matchesScope(file.path, scope));
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 evaluateWithPort(port, request) {
306
+ async function evaluateRuleBatchWithPort(port, requests) {
301
307
  try {
302
- const result = await port.evaluate(request);
303
- return result.kind === "RULE" ? result : { kind: "RULE", status: "FAILED", reason: "INVALID_RESPONSE" };
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 { kind: "RULE", status: "FAILED", reason: "API_ERROR" };
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
- // packages/core/src/evaluation/evaluate-rule.ts
318
- async function evaluateRule(input, dependencies) {
319
- const selection = selectRuleEvidence(input.turn, input.rule, input.evidencePolicy);
320
- if (selection.status === "SKIPPED") {
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
- if (selection.status === "UNAVAILABLE") {
325
- const context = buildReviewContext(input.turn, input.rule, []);
326
- return { ...context, outcome: "UNAVAILABLE", reason: selection.reason };
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 context = buildReviewContext(input.turn, input.rule, selection.evidence.files);
329
- const request = buildJevRequest(input.turn.task, input.rule, selection.evidence);
330
- const evaluation = await evaluateWithPort(dependencies.jev, request);
331
- if (evaluation.status === "FAILED") {
332
- return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
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
- const gate = evaluateGate(input.rule.severity, evaluation.noul.violationProbability, input.gateConfig);
335
- return { ...context, ...gate };
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/evaluate-rules.ts
391
- async function evaluateRules(input, dependencies) {
392
- const results = [];
393
- for (const rule of input.rules) {
394
- results.push(await evaluateRuleResult(input, rule, dependencies));
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
- return aggregateReview(input.turn.id, results);
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
- async function evaluateRuleResult(input, rule, dependencies) {
399
- if (rule.status === "INVALID") {
400
- return invalidRuleResult(input.turn.id, rule);
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
- return invalidConfigResult(input.turn, rule.rule);
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
- const ruleInput = {
406
- turn: input.turn,
407
- rule: rule.rule,
408
- gateConfig: input.gateConfig.config,
409
- evidencePolicy: input.evidencePolicy
410
- };
411
- return evaluateRule(ruleInput, dependencies);
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 scope = null;
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 === "scope") {
473
- if (entry.value === "") {
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
- scope = entry.value;
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
- return { ok: true, value: { severity, scope } };
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(ignore, ignore);
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(ignore, ignore);
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 ignore() {
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 join5 } from "path";
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(join5(directory, SKILL_ENTRY_FILE))) {
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(),