@pablozrrrr/jevguard 0.2.0 → 0.2.3

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