@pablozrrrr/jevguard 0.2.3 → 0.3.1
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 +240 -79
- package/cli/main.js +4155 -1720
- package/index.js +3697 -1912
- package/package.json +14 -2
- package/skills/jev-init/SKILL.md +1 -1
- package/skills/jev-init/validate-init.js +1686 -843
- package/skills/jevguard-rules/SKILL.md +1 -1
- package/skills/jevguard-rules/validate-rules.js +1563 -724
|
@@ -3,38 +3,65 @@
|
|
|
3
3
|
import { readFileSync, statSync } from "fs";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
|
|
6
|
-
// packages/core/src/
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// packages/core/src/builtins/complexity.ts
|
|
7
|
+
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
8
|
+
var COMPLEXITY_GATE_CONFIG = {
|
|
9
|
+
version: 1,
|
|
10
|
+
thresholds: {
|
|
11
|
+
error: { warn: 0.4, fail: 0.7 },
|
|
12
|
+
warning: { warn: 0.5 }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var COMPLEXITY_INSTRUCTIONS = "Does this attributed change introduce material complexity that is disproportionate to, or not reasonably necessary for, completing the user's task? Necessary supporting changes, tests, validation, error handling, and complexity explicitly required by the task should not count as unnecessary complexity. Return the probability from 0 to 1 that the change introduces disproportionate or unnecessary complexity.";
|
|
16
|
+
var COMPLEXITY_DESCRIPTION = "Material complexity introduced by the attributed change that is disproportionate to the problem and not reasonably necessary for completing the task.";
|
|
17
|
+
var COMPLEXITY_VIOLATION = "The change adds unnecessary abstractions, layers or indirections without proportional gain, new dependencies without clear need, excessive configuration, premature generalization, or structure materially larger than the problem requires.";
|
|
18
|
+
var COMPLEXITY_ALLOWED = "Complexity explicitly required by the task, and necessary supporting changes, tests, validation, error handling, auxiliary changes, or following an existing codebase abstraction to avoid breaking its pattern.";
|
|
19
|
+
function buildComplexityQuestion() {
|
|
12
20
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
violation: rule.violation,
|
|
22
|
-
...rule.allowed === null ? {} : { allowed: rule.allowed }
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
change: { files: evidence.files, diff: evidence.diff }
|
|
21
|
+
type: "noul",
|
|
22
|
+
instructions: COMPLEXITY_INSTRUCTIONS,
|
|
23
|
+
criteria: {
|
|
24
|
+
id: COMPLEXITY_CHECK_ID,
|
|
25
|
+
description: COMPLEXITY_DESCRIPTION,
|
|
26
|
+
violation: COMPLEXITY_VIOLATION,
|
|
27
|
+
allowed: COMPLEXITY_ALLOWED
|
|
28
|
+
}
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
function buildReviewContext(turn, rule, scopedPaths) {
|
|
31
|
+
function buildComplexityContext(turn, scopedPaths) {
|
|
30
32
|
return {
|
|
31
|
-
kind: "
|
|
33
|
+
kind: "BUILT_IN",
|
|
32
34
|
turnId: turn.id,
|
|
33
|
-
ruleId:
|
|
34
|
-
|
|
35
|
+
ruleId: null,
|
|
36
|
+
checkId: COMPLEXITY_CHECK_ID,
|
|
37
|
+
severity: "warning",
|
|
35
38
|
scopedPaths
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
// packages/core/src/evaluation/evaluate-with-port.ts
|
|
42
|
+
async function evaluateRuleBatchWithPort(port, requests) {
|
|
43
|
+
try {
|
|
44
|
+
const results = await port.evaluateRuleBatch(requests);
|
|
45
|
+
if (results.length !== requests.length) {
|
|
46
|
+
return requests.map(() => ruleFailure("INVALID_RESPONSE"));
|
|
47
|
+
}
|
|
48
|
+
return results.map((result) => result.kind === "RULE" ? result : ruleFailure("INVALID_RESPONSE"));
|
|
49
|
+
} catch {
|
|
50
|
+
return requests.map(() => ruleFailure("API_ERROR"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function ruleFailure(reason) {
|
|
54
|
+
return { kind: "RULE", status: "FAILED", reason };
|
|
55
|
+
}
|
|
56
|
+
async function evaluateBuiltInBatchWithPort(port, request) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await port.evaluate(request);
|
|
59
|
+
return result.kind === "BUILT_IN_BATCH" ? result : { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
60
|
+
} catch {
|
|
61
|
+
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "API_ERROR" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
// packages/core/src/evidence/defaults.ts
|
|
39
66
|
var DEFAULT_MAX_DIFF_LENGTH = 1e5;
|
|
40
67
|
var DEFAULT_EVIDENCE_POLICY = {
|
|
@@ -306,97 +333,275 @@ function evaluateWarningGate(probability, thresholds) {
|
|
|
306
333
|
return { outcome: "WARN", violationProbability: probability };
|
|
307
334
|
}
|
|
308
335
|
|
|
309
|
-
// packages/core/src/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
336
|
+
// packages/core/src/ports/types.ts
|
|
337
|
+
var SCOPE_CREEP_ANSWER = "scopeCreep";
|
|
338
|
+
var COMPLEXITY_ANSWER = "complexity";
|
|
339
|
+
|
|
340
|
+
// packages/core/src/builtins/scope-creep.ts
|
|
341
|
+
var SCOPE_CREEP_CHECK_ID = "SCOPE-CREEP";
|
|
342
|
+
var SCOPE_CREEP_GATE_CONFIG = {
|
|
343
|
+
version: 1,
|
|
344
|
+
thresholds: {
|
|
345
|
+
error: { warn: 0.65, fail: 0.9 },
|
|
346
|
+
warning: { warn: 0.6 }
|
|
319
347
|
}
|
|
348
|
+
};
|
|
349
|
+
var SCOPE_CREEP_INSTRUCTIONS = "Answer true when the attributed change contains scope creep: material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task. Answer false when every change is requested work, or supporting, testing, or documentation work reasonably necessary to complete the requested work, including immaterial incidental edits. Return the probability from 0 to 1 that the change is scope creep.";
|
|
350
|
+
var SCOPE_CREEP_DESCRIPTION = "A change is scope creep when it introduces material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete it.";
|
|
351
|
+
var SCOPE_CREEP_VIOLATION = "The change adds material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task.";
|
|
352
|
+
var SCOPE_CREEP_ALLOWED = "Changes the task requested, and supporting, testing, and documentation changes reasonably necessary to complete the requested work, including immaterial incidental edits.";
|
|
353
|
+
function buildScopeCreepQuestion() {
|
|
354
|
+
return {
|
|
355
|
+
type: "noul",
|
|
356
|
+
instructions: SCOPE_CREEP_INSTRUCTIONS,
|
|
357
|
+
criteria: {
|
|
358
|
+
id: SCOPE_CREEP_CHECK_ID,
|
|
359
|
+
description: SCOPE_CREEP_DESCRIPTION,
|
|
360
|
+
violation: SCOPE_CREEP_VIOLATION,
|
|
361
|
+
allowed: SCOPE_CREEP_ALLOWED
|
|
362
|
+
}
|
|
363
|
+
};
|
|
320
364
|
}
|
|
321
|
-
function
|
|
322
|
-
return {
|
|
365
|
+
function buildScopeCreepContext(turn, scopedPaths) {
|
|
366
|
+
return {
|
|
367
|
+
kind: "BUILT_IN",
|
|
368
|
+
turnId: turn.id,
|
|
369
|
+
ruleId: null,
|
|
370
|
+
checkId: SCOPE_CREEP_CHECK_ID,
|
|
371
|
+
severity: "error",
|
|
372
|
+
scopedPaths
|
|
373
|
+
};
|
|
323
374
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
375
|
+
|
|
376
|
+
// packages/core/src/builtins/evaluate-builtins.ts
|
|
377
|
+
function buildBuiltInBatchRequest(task, evidence) {
|
|
378
|
+
return {
|
|
379
|
+
kind: "BUILT_IN_BATCH",
|
|
380
|
+
task,
|
|
381
|
+
questions: {
|
|
382
|
+
[SCOPE_CREEP_ANSWER]: buildScopeCreepQuestion(),
|
|
383
|
+
[COMPLEXITY_ANSWER]: buildComplexityQuestion()
|
|
384
|
+
},
|
|
385
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
386
|
+
};
|
|
331
387
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
388
|
+
async function evaluateBuiltIns(input, dependencies) {
|
|
389
|
+
const selection = selectTurnEvidence(input.turn, input.evidencePolicy);
|
|
390
|
+
if (selection.status === "SKIPPED") {
|
|
391
|
+
return {
|
|
392
|
+
scopeCreep: {
|
|
393
|
+
...buildScopeCreepContext(input.turn, []),
|
|
394
|
+
outcome: "SKIPPED",
|
|
395
|
+
reason: selection.reason
|
|
396
|
+
},
|
|
397
|
+
complexity: {
|
|
398
|
+
...buildComplexityContext(input.turn, []),
|
|
399
|
+
outcome: "SKIPPED",
|
|
400
|
+
reason: selection.reason
|
|
401
|
+
}
|
|
402
|
+
};
|
|
345
403
|
}
|
|
346
|
-
if (
|
|
347
|
-
return {
|
|
404
|
+
if (selection.status === "UNAVAILABLE") {
|
|
405
|
+
return {
|
|
406
|
+
scopeCreep: {
|
|
407
|
+
...buildScopeCreepContext(input.turn, []),
|
|
408
|
+
outcome: "UNAVAILABLE",
|
|
409
|
+
reason: selection.reason
|
|
410
|
+
},
|
|
411
|
+
complexity: {
|
|
412
|
+
...buildComplexityContext(input.turn, []),
|
|
413
|
+
outcome: "UNAVAILABLE",
|
|
414
|
+
reason: selection.reason
|
|
415
|
+
}
|
|
416
|
+
};
|
|
348
417
|
}
|
|
349
|
-
|
|
350
|
-
|
|
418
|
+
const evidence = selection.evidence;
|
|
419
|
+
const request = buildBuiltInBatchRequest(input.turn.task, evidence);
|
|
420
|
+
const evaluation = await evaluateBuiltInBatchWithPort(dependencies.jev, request);
|
|
421
|
+
const scopeCreepContext = buildScopeCreepContext(input.turn, evidence.files);
|
|
422
|
+
const complexityContext = buildComplexityContext(input.turn, evidence.files);
|
|
423
|
+
if (evaluation.status === "FAILED") {
|
|
424
|
+
return {
|
|
425
|
+
scopeCreep: { ...scopeCreepContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" },
|
|
426
|
+
complexity: { ...complexityContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" }
|
|
427
|
+
};
|
|
351
428
|
}
|
|
352
|
-
return sliceByFile(applicableFiles, policy);
|
|
353
|
-
}
|
|
354
|
-
function wholeSlice(files, diff) {
|
|
355
|
-
const paths = files.map((file) => file.path);
|
|
356
429
|
return {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
path: paths.length === 1 ? paths[0] ?? null : null,
|
|
360
|
-
hunkOrdinal: null,
|
|
361
|
-
files: paths,
|
|
362
|
-
diff
|
|
430
|
+
scopeCreep: gateScopeCreep(evaluation.answers.scopeCreep, scopeCreepContext),
|
|
431
|
+
complexity: gateComplexity(evaluation.answers.complexity, complexityContext)
|
|
363
432
|
};
|
|
364
433
|
}
|
|
365
|
-
function
|
|
366
|
-
|
|
367
|
-
|
|
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);
|
|
434
|
+
function gateScopeCreep(answer, context) {
|
|
435
|
+
if (answer.status === "FAILED") {
|
|
436
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
377
437
|
}
|
|
378
|
-
return { status: "PLANNED", slices };
|
|
379
|
-
}
|
|
380
|
-
function fileSlice(index, file) {
|
|
381
438
|
return {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
path: file.path,
|
|
385
|
-
hunkOrdinal: null,
|
|
386
|
-
files: [file.path],
|
|
387
|
-
diff: file.patch
|
|
439
|
+
...context,
|
|
440
|
+
...evaluateGate("error", answer.noul.violationProbability, SCOPE_CREEP_GATE_CONFIG)
|
|
388
441
|
};
|
|
389
442
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return null;
|
|
443
|
+
function gateComplexity(answer, context) {
|
|
444
|
+
if (answer.status === "FAILED") {
|
|
445
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
394
446
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
447
|
+
return {
|
|
448
|
+
...context,
|
|
449
|
+
...evaluateGate("warning", answer.noul.violationProbability, COMPLEXITY_GATE_CONFIG)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
// packages/core/src/concurrency/fifo-port.ts
|
|
453
|
+
function createFifoJevPort(port, config) {
|
|
454
|
+
const maxConcurrency = config.maxConcurrency;
|
|
455
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
456
|
+
throw new RangeError("maxConcurrency must be a positive integer");
|
|
398
457
|
}
|
|
399
|
-
const
|
|
458
|
+
const queue = [];
|
|
459
|
+
let active = 0;
|
|
460
|
+
function drain() {
|
|
461
|
+
while (active < maxConcurrency && queue.length > 0) {
|
|
462
|
+
const pending = queue.shift();
|
|
463
|
+
if (pending === undefined) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
active += 1;
|
|
467
|
+
start(pending);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function start(pending) {
|
|
471
|
+
if (pending.kind === "BATCH") {
|
|
472
|
+
Promise.resolve().then(() => port.evaluateRuleBatch(pending.requests)).then((results) => {
|
|
473
|
+
active -= 1;
|
|
474
|
+
pending.settle(results);
|
|
475
|
+
drain();
|
|
476
|
+
}, (error) => {
|
|
477
|
+
active -= 1;
|
|
478
|
+
pending.fail(error);
|
|
479
|
+
drain();
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
484
|
+
active -= 1;
|
|
485
|
+
pending.settle(result);
|
|
486
|
+
drain();
|
|
487
|
+
}, (error) => {
|
|
488
|
+
active -= 1;
|
|
489
|
+
pending.fail(error);
|
|
490
|
+
drain();
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
evaluate(request) {
|
|
495
|
+
return new Promise((settle, fail) => {
|
|
496
|
+
queue.push({ kind: "SINGLE", request, settle, fail });
|
|
497
|
+
drain();
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
evaluateRuleBatch(requests) {
|
|
501
|
+
return new Promise((settle, fail) => {
|
|
502
|
+
queue.push({ kind: "BATCH", requests, settle, fail });
|
|
503
|
+
drain();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// packages/core/src/evaluation/build-context.ts
|
|
509
|
+
function buildReviewContext(turn, rule, scopedPaths) {
|
|
510
|
+
return {
|
|
511
|
+
kind: "RULE",
|
|
512
|
+
turnId: turn.id,
|
|
513
|
+
ruleId: rule.id,
|
|
514
|
+
severity: rule.severity,
|
|
515
|
+
scopedPaths
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// packages/core/src/evaluation/build-request.ts
|
|
519
|
+
var NOUL_INSTRUCTIONS = "Answer true when the attributed change violates the rule's Violation, excluding behavior described by the rule's Allowed exception. Answer false when the change does not violate the rule, including allowed behavior. Return the probability from 0 to 1 that the change violates the rule.";
|
|
520
|
+
function buildJevRequest(task, rule, evidence) {
|
|
521
|
+
return {
|
|
522
|
+
kind: "RULE",
|
|
523
|
+
task,
|
|
524
|
+
question: {
|
|
525
|
+
type: "noul",
|
|
526
|
+
instructions: NOUL_INSTRUCTIONS,
|
|
527
|
+
criteria: {
|
|
528
|
+
id: rule.id,
|
|
529
|
+
description: rule.description,
|
|
530
|
+
violation: rule.violation,
|
|
531
|
+
...rule.allowed === null ? {} : { allowed: rule.allowed }
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// packages/core/src/evidence/plan-slices.ts
|
|
538
|
+
var HUNK_MARKER = /^@@ [^\n]*@@/gm;
|
|
539
|
+
function planRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
540
|
+
if (hasNoAttributedPatch(turn)) {
|
|
541
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
542
|
+
}
|
|
543
|
+
const applicableFiles = selectApplicableFiles(turn.files, rule.scope, rule.evidence, policy);
|
|
544
|
+
if (applicableFiles.length === 0) {
|
|
545
|
+
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
546
|
+
}
|
|
547
|
+
const diff = assembleDiff(applicableFiles);
|
|
548
|
+
if (diff.trim() === "") {
|
|
549
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
550
|
+
}
|
|
551
|
+
if (applicableFiles.some((file) => !checkFilePath(file.path, policy).allowed)) {
|
|
552
|
+
return { status: "UNAVAILABLE", reason: "BLOCKED_EVIDENCE" };
|
|
553
|
+
}
|
|
554
|
+
if (diff.length <= policy.maxDiffLength) {
|
|
555
|
+
return { status: "PLANNED", slices: [wholeSlice(applicableFiles, diff)] };
|
|
556
|
+
}
|
|
557
|
+
return sliceByFile(applicableFiles, policy);
|
|
558
|
+
}
|
|
559
|
+
function wholeSlice(files, diff) {
|
|
560
|
+
const paths = files.map((file) => file.path);
|
|
561
|
+
return {
|
|
562
|
+
index: 0,
|
|
563
|
+
kind: "WHOLE",
|
|
564
|
+
path: paths.length === 1 ? paths[0] ?? null : null,
|
|
565
|
+
hunkOrdinal: null,
|
|
566
|
+
files: paths,
|
|
567
|
+
diff
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function sliceByFile(files, policy) {
|
|
571
|
+
const slices = [];
|
|
572
|
+
for (const file of files) {
|
|
573
|
+
if (file.patch.length <= policy.maxDiffLength) {
|
|
574
|
+
slices.push(fileSlice(slices.length, file));
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const hunks = sliceHunks(slices.length, file, policy);
|
|
578
|
+
if (hunks === null) {
|
|
579
|
+
return { status: "UNAVAILABLE", reason: "OVERSIZED_DIFF" };
|
|
580
|
+
}
|
|
581
|
+
slices.push(...hunks);
|
|
582
|
+
}
|
|
583
|
+
return { status: "PLANNED", slices };
|
|
584
|
+
}
|
|
585
|
+
function fileSlice(index, file) {
|
|
586
|
+
return {
|
|
587
|
+
index,
|
|
588
|
+
kind: "FILE",
|
|
589
|
+
path: file.path,
|
|
590
|
+
hunkOrdinal: null,
|
|
591
|
+
files: [file.path],
|
|
592
|
+
diff: file.patch
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function sliceHunks(startIndex, file, policy) {
|
|
596
|
+
const starts = hunkStarts(file.patch);
|
|
597
|
+
if (starts.length === 0) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
const firstStart = starts[0];
|
|
601
|
+
if (firstStart === undefined) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const preamble = file.patch.slice(0, firstStart);
|
|
400
605
|
const slices = [];
|
|
401
606
|
for (let ordinal = 0;ordinal < starts.length; ordinal += 1) {
|
|
402
607
|
const start = starts[ordinal];
|
|
@@ -711,610 +916,416 @@ function invalidConfigResult(turn, rule) {
|
|
|
711
916
|
reason: "INVALID_CONFIG"
|
|
712
917
|
};
|
|
713
918
|
}
|
|
714
|
-
// packages/core/src/
|
|
715
|
-
var
|
|
716
|
-
var
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
919
|
+
// packages/core/src/evidence/parse-unified-diff.ts
|
|
920
|
+
var GIT_HEADER = "diff --git ";
|
|
921
|
+
var OLD_FILE_MARKER = "--- ";
|
|
922
|
+
var NEW_FILE_MARKER = "+++ ";
|
|
923
|
+
var DEV_NULL = "/dev/null";
|
|
924
|
+
var OLD_PREFIX = "a/";
|
|
925
|
+
var NEW_PREFIX = "b/";
|
|
926
|
+
var NO_NEWLINE_MARKER = "\\ ";
|
|
927
|
+
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/;
|
|
928
|
+
var COMBINED_HEADERS = ["diff --cc ", "diff --combined "];
|
|
929
|
+
var RENAME_FROM_HEADERS = ["rename from ", "copy from "];
|
|
930
|
+
var RENAME_TO_HEADERS = ["rename to ", "copy to "];
|
|
931
|
+
var KNOWN_EXTENDED_HEADERS = [
|
|
932
|
+
"old mode ",
|
|
933
|
+
"new mode ",
|
|
934
|
+
"deleted file mode ",
|
|
935
|
+
"new file mode ",
|
|
936
|
+
"similarity index ",
|
|
937
|
+
"dissimilarity index ",
|
|
938
|
+
"index ",
|
|
939
|
+
...RENAME_FROM_HEADERS,
|
|
940
|
+
...RENAME_TO_HEADERS
|
|
941
|
+
];
|
|
942
|
+
function parseUnifiedDiff(text) {
|
|
943
|
+
const diff = splitLines(text);
|
|
944
|
+
const first = diff.text.findIndex((line) => line.trim() !== "");
|
|
945
|
+
if (first === -1) {
|
|
946
|
+
return { status: "PARSED", files: [] };
|
|
947
|
+
}
|
|
948
|
+
if (diff.text.some(isCombinedHeader)) {
|
|
949
|
+
return invalid("COMBINED_DIFF");
|
|
950
|
+
}
|
|
951
|
+
const format = detectFormat(diff.text, first);
|
|
952
|
+
if (format === null) {
|
|
953
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
954
|
+
}
|
|
955
|
+
return collectFiles(diff, first, format);
|
|
956
|
+
}
|
|
957
|
+
function invalid(reason) {
|
|
958
|
+
return { status: "INVALID", reason };
|
|
959
|
+
}
|
|
960
|
+
function splitLines(text) {
|
|
961
|
+
const raw = text.split(`
|
|
962
|
+
`);
|
|
963
|
+
const endsWithNewline = text.endsWith(`
|
|
964
|
+
`);
|
|
965
|
+
if (endsWithNewline) {
|
|
966
|
+
raw.pop();
|
|
721
967
|
}
|
|
722
|
-
|
|
968
|
+
const stripped = raw.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
969
|
+
return { raw, text: stripped, endsWithNewline };
|
|
723
970
|
}
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
|
|
971
|
+
function isCombinedHeader(line) {
|
|
972
|
+
return COMBINED_HEADERS.some((header) => line.startsWith(header));
|
|
973
|
+
}
|
|
974
|
+
function detectFormat(text, first) {
|
|
975
|
+
if ((text[first] ?? "").startsWith(GIT_HEADER)) {
|
|
976
|
+
return "GIT";
|
|
977
|
+
}
|
|
978
|
+
if (text.some((line) => line.startsWith(GIT_HEADER))) {
|
|
727
979
|
return null;
|
|
728
980
|
}
|
|
729
|
-
return (
|
|
981
|
+
return isPlainSectionStart(text, first) ? "PLAIN" : null;
|
|
730
982
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
for (const line of collectMetadataLines(lines)) {
|
|
742
|
-
const entry = parseMetadataEntry(line);
|
|
743
|
-
if (entry === null) {
|
|
744
|
-
return { ok: false, code: "MALFORMED_METADATA" };
|
|
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
|
-
}
|
|
759
|
-
if (seenKeys.has(entry.key)) {
|
|
760
|
-
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
983
|
+
function isPlainSectionStart(text, index) {
|
|
984
|
+
return (text[index] ?? "").startsWith(OLD_FILE_MARKER) && (text[index + 1] ?? "").startsWith(NEW_FILE_MARKER);
|
|
985
|
+
}
|
|
986
|
+
function isSectionStart(text, index, format) {
|
|
987
|
+
return format === "GIT" ? (text[index] ?? "").startsWith(GIT_HEADER) : isPlainSectionStart(text, index);
|
|
988
|
+
}
|
|
989
|
+
function isBlankTail(text, from) {
|
|
990
|
+
for (let index = from;index < text.length; index += 1) {
|
|
991
|
+
if ((text[index] ?? "").trim() !== "") {
|
|
992
|
+
return false;
|
|
761
993
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
function collectFiles(diff, first, format) {
|
|
998
|
+
const files = [];
|
|
999
|
+
let cursor = first;
|
|
1000
|
+
while (cursor < diff.text.length) {
|
|
1001
|
+
const section = format === "GIT" ? readGitSection(diff, cursor) : readPlainSection(diff, cursor);
|
|
1002
|
+
if (section.status === "INVALID") {
|
|
1003
|
+
return section;
|
|
770
1004
|
}
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
if (parsed === null) {
|
|
774
|
-
return { ok: false, code: "MALFORMED_METADATA" };
|
|
775
|
-
}
|
|
776
|
-
evidence = parsed;
|
|
777
|
-
continue;
|
|
1005
|
+
if (section.file !== null) {
|
|
1006
|
+
files.push(section.file);
|
|
778
1007
|
}
|
|
779
|
-
|
|
1008
|
+
cursor = section.next;
|
|
780
1009
|
}
|
|
781
|
-
|
|
782
|
-
|
|
1010
|
+
return { status: "PARSED", files };
|
|
1011
|
+
}
|
|
1012
|
+
function readGitSection(diff, start) {
|
|
1013
|
+
const header = parseGitHeader(diff.text[start] ?? "");
|
|
1014
|
+
if (header.status === "INVALID") {
|
|
1015
|
+
return header;
|
|
783
1016
|
}
|
|
784
|
-
|
|
785
|
-
|
|
1017
|
+
const extended = readExtendedHeaders(diff.text, start + 1);
|
|
1018
|
+
if (extended.status === "INVALID") {
|
|
1019
|
+
return extended;
|
|
786
1020
|
}
|
|
787
|
-
|
|
788
|
-
|
|
1021
|
+
if (!(diff.text[extended.next] ?? "").startsWith(OLD_FILE_MARKER)) {
|
|
1022
|
+
return readSectionWithoutContent(diff.text, extended.next);
|
|
1023
|
+
}
|
|
1024
|
+
const paths = readFilePaths(diff.text, extended.next, "GIT");
|
|
1025
|
+
if (paths.status === "INVALID") {
|
|
1026
|
+
return paths;
|
|
1027
|
+
}
|
|
1028
|
+
const failure = checkGitPaths(header, extended, paths);
|
|
1029
|
+
if (failure !== null) {
|
|
1030
|
+
return failure;
|
|
1031
|
+
}
|
|
1032
|
+
return readContent(diff, start, extended.next + 2, paths, "GIT");
|
|
789
1033
|
}
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (matchSectionHeading(line) !== null) {
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
if (line.trim() !== "") {
|
|
797
|
-
collected.push(line);
|
|
798
|
-
}
|
|
1034
|
+
function readSectionWithoutContent(text, index) {
|
|
1035
|
+
if (index >= text.length || text[index]?.startsWith(GIT_HEADER)) {
|
|
1036
|
+
return { status: "OK", file: null, next: index };
|
|
799
1037
|
}
|
|
800
|
-
return
|
|
1038
|
+
return isBlankTail(text, index) ? { status: "OK", file: null, next: text.length } : invalid("UNRECOGNIZED_DIFF");
|
|
801
1039
|
}
|
|
802
|
-
function
|
|
803
|
-
const
|
|
804
|
-
if (
|
|
805
|
-
return
|
|
1040
|
+
function parseGitHeader(line) {
|
|
1041
|
+
const rest = line.slice(GIT_HEADER.length);
|
|
1042
|
+
if (rest.startsWith('"') || rest.includes(' "')) {
|
|
1043
|
+
return invalid("QUOTED_PATH");
|
|
806
1044
|
}
|
|
807
|
-
|
|
1045
|
+
if (!rest.startsWith(OLD_PREFIX)) {
|
|
1046
|
+
return invalid("UNSUPPORTED_PREFIX");
|
|
1047
|
+
}
|
|
1048
|
+
const split = splitGitHeaderPaths(rest);
|
|
1049
|
+
if (split === null) {
|
|
1050
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
1051
|
+
}
|
|
1052
|
+
if (split.oldPath === "" || split.newPath === "") {
|
|
1053
|
+
return invalid("MISSING_PATH");
|
|
1054
|
+
}
|
|
1055
|
+
return { status: "OK", ...split };
|
|
808
1056
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1057
|
+
function splitGitHeaderPaths(rest) {
|
|
1058
|
+
const separator = ` ${NEW_PREFIX}`;
|
|
1059
|
+
const pathLength = (rest.length - OLD_PREFIX.length - separator.length) / 2;
|
|
1060
|
+
if (Number.isInteger(pathLength) && pathLength > 0) {
|
|
1061
|
+
const oldPath = rest.slice(OLD_PREFIX.length, OLD_PREFIX.length + pathLength);
|
|
1062
|
+
const tail = rest.slice(OLD_PREFIX.length + pathLength);
|
|
1063
|
+
if (tail === `${separator}${oldPath}`) {
|
|
1064
|
+
return { oldPath, newPath: oldPath };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const index = rest.indexOf(separator, OLD_PREFIX.length);
|
|
1068
|
+
if (index === -1) {
|
|
1069
|
+
return null;
|
|
816
1070
|
}
|
|
1071
|
+
return {
|
|
1072
|
+
oldPath: rest.slice(OLD_PREFIX.length, index),
|
|
1073
|
+
newPath: rest.slice(index + separator.length)
|
|
1074
|
+
};
|
|
817
1075
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return
|
|
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
|
-
}
|
|
838
|
-
|
|
839
|
-
// packages/core/src/rules/sections.ts
|
|
840
|
-
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
841
|
-
function parseRuleSections(lines) {
|
|
842
|
-
const collected = collectSections(lines);
|
|
843
|
-
if (!collected.ok) {
|
|
844
|
-
return collected;
|
|
845
|
-
}
|
|
846
|
-
return validateSections(collected.value);
|
|
847
|
-
}
|
|
848
|
-
function collectSections(lines) {
|
|
849
|
-
const sections = new Map;
|
|
850
|
-
let current = null;
|
|
851
|
-
let content = [];
|
|
852
|
-
for (const line of lines) {
|
|
853
|
-
const heading = matchSectionHeading(line);
|
|
854
|
-
if (heading === null) {
|
|
855
|
-
if (current !== null) {
|
|
856
|
-
content.push(line);
|
|
857
|
-
}
|
|
858
|
-
continue;
|
|
1076
|
+
function readExtendedHeaders(text, start) {
|
|
1077
|
+
let movedFrom = null;
|
|
1078
|
+
let movedTo = null;
|
|
1079
|
+
let cursor = start;
|
|
1080
|
+
while (cursor < text.length) {
|
|
1081
|
+
const line = text[cursor] ?? "";
|
|
1082
|
+
if (line.startsWith("GIT binary patch") || line.startsWith("Binary files ")) {
|
|
1083
|
+
return invalid("BINARY_PATCH");
|
|
859
1084
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
return { ok: false, code: "UNKNOWN_SECTION" };
|
|
1085
|
+
const fromValue = headerValue(line, RENAME_FROM_HEADERS);
|
|
1086
|
+
const toValue = headerValue(line, RENAME_TO_HEADERS);
|
|
1087
|
+
if (fromValue?.startsWith('"') || toValue?.startsWith('"')) {
|
|
1088
|
+
return invalid("QUOTED_PATH");
|
|
865
1089
|
}
|
|
866
|
-
|
|
867
|
-
|
|
1090
|
+
movedFrom = fromValue ?? movedFrom;
|
|
1091
|
+
movedTo = toValue ?? movedTo;
|
|
1092
|
+
if (!KNOWN_EXTENDED_HEADERS.some((header) => line.startsWith(header))) {
|
|
1093
|
+
break;
|
|
868
1094
|
}
|
|
869
|
-
|
|
870
|
-
content = [];
|
|
871
|
-
}
|
|
872
|
-
if (current !== null) {
|
|
873
|
-
sections.set(current, joinContent(content));
|
|
1095
|
+
cursor += 1;
|
|
874
1096
|
}
|
|
875
|
-
return {
|
|
1097
|
+
return { status: "OK", next: cursor, movedFrom, movedTo };
|
|
876
1098
|
}
|
|
877
|
-
function
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
885
|
-
return
|
|
1099
|
+
function headerValue(line, headers) {
|
|
1100
|
+
const header = headers.find((candidate) => line.startsWith(candidate));
|
|
1101
|
+
return header === undefined ? null : line.slice(header.length);
|
|
1102
|
+
}
|
|
1103
|
+
function readFilePaths(text, index, format) {
|
|
1104
|
+
const oldLine = text[index] ?? "";
|
|
1105
|
+
const newLine = text[index + 1] ?? "";
|
|
1106
|
+
if (!oldLine.startsWith(OLD_FILE_MARKER) || !newLine.startsWith(NEW_FILE_MARKER)) {
|
|
1107
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
886
1108
|
}
|
|
887
|
-
|
|
888
|
-
|
|
1109
|
+
const oldValue = cutAtTab(oldLine.slice(OLD_FILE_MARKER.length));
|
|
1110
|
+
const newValue = cutAtTab(newLine.slice(NEW_FILE_MARKER.length));
|
|
1111
|
+
if (oldValue.startsWith('"') || newValue.startsWith('"')) {
|
|
1112
|
+
return invalid("QUOTED_PATH");
|
|
889
1113
|
}
|
|
890
|
-
|
|
891
|
-
|
|
1114
|
+
const stripped = stripPrefixes(oldValue, newValue, format);
|
|
1115
|
+
if (stripped === null) {
|
|
1116
|
+
return invalid("UNSUPPORTED_PREFIX");
|
|
892
1117
|
}
|
|
893
|
-
if (
|
|
894
|
-
return
|
|
1118
|
+
if (isMissing(stripped.oldPath) && isMissing(stripped.newPath)) {
|
|
1119
|
+
return invalid("MISSING_PATH");
|
|
895
1120
|
}
|
|
896
|
-
|
|
897
|
-
|
|
1121
|
+
const oldPath = stripped.oldPath === DEV_NULL ? null : stripped.oldPath;
|
|
1122
|
+
const newPath = stripped.newPath === DEV_NULL ? null : stripped.newPath;
|
|
1123
|
+
if (oldPath === "" || newPath === "") {
|
|
1124
|
+
return invalid("MISSING_PATH");
|
|
898
1125
|
}
|
|
899
|
-
return {
|
|
1126
|
+
return { status: "OK", oldPath, newPath };
|
|
900
1127
|
}
|
|
901
|
-
function
|
|
902
|
-
return
|
|
903
|
-
`).trim();
|
|
1128
|
+
function isMissing(path) {
|
|
1129
|
+
return path === "" || path === DEV_NULL;
|
|
904
1130
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
function isValidRuleId(id) {
|
|
909
|
-
return RULE_ID.test(id);
|
|
1131
|
+
function cutAtTab(value) {
|
|
1132
|
+
const tabIndex = value.indexOf("\t");
|
|
1133
|
+
return tabIndex === -1 ? value : value.slice(0, tabIndex);
|
|
910
1134
|
}
|
|
911
|
-
function
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
if (current !== null) {
|
|
922
|
-
current.body.push(line);
|
|
923
|
-
}
|
|
1135
|
+
function stripPrefixes(oldValue, newValue, format) {
|
|
1136
|
+
const hasOldPrefix = oldValue.startsWith(OLD_PREFIX);
|
|
1137
|
+
const hasNewPrefix = newValue.startsWith(NEW_PREFIX);
|
|
1138
|
+
const oldAllowed = hasOldPrefix || oldValue === DEV_NULL;
|
|
1139
|
+
const newAllowed = hasNewPrefix || newValue === DEV_NULL;
|
|
1140
|
+
if (oldAllowed && newAllowed) {
|
|
1141
|
+
return {
|
|
1142
|
+
oldPath: hasOldPrefix ? oldValue.slice(OLD_PREFIX.length) : oldValue,
|
|
1143
|
+
newPath: hasNewPrefix ? newValue.slice(NEW_PREFIX.length) : newValue
|
|
1144
|
+
};
|
|
924
1145
|
}
|
|
925
|
-
|
|
1146
|
+
if (format === "PLAIN" && !hasOldPrefix && !hasNewPrefix) {
|
|
1147
|
+
return { oldPath: oldValue, newPath: newValue };
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
926
1150
|
}
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
|
|
1151
|
+
function checkGitPaths(header, extended, paths) {
|
|
1152
|
+
const oldMatches = paths.oldPath === null || paths.oldPath === header.oldPath;
|
|
1153
|
+
const newMatches = paths.newPath === null || paths.newPath === header.newPath;
|
|
1154
|
+
if (!oldMatches || !newMatches) {
|
|
1155
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
930
1156
|
}
|
|
931
|
-
|
|
932
|
-
|
|
1157
|
+
const movedByHeader = extended.movedFrom !== null && extended.movedTo !== null && extended.movedFrom !== extended.movedTo;
|
|
1158
|
+
if (header.oldPath !== header.newPath || movedByHeader) {
|
|
1159
|
+
return invalid("RENAMED_PATH");
|
|
933
1160
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
function readPlainSection(diff, start) {
|
|
1164
|
+
const paths = readFilePaths(diff.text, start, "PLAIN");
|
|
1165
|
+
if (paths.status === "INVALID") {
|
|
1166
|
+
return paths;
|
|
937
1167
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return ruleCandidateFailure(sections.code, block.id);
|
|
1168
|
+
if (paths.oldPath !== null && paths.newPath !== null && paths.oldPath !== paths.newPath) {
|
|
1169
|
+
return invalid("RENAMED_PATH");
|
|
941
1170
|
}
|
|
942
|
-
return
|
|
943
|
-
status: "PARSED",
|
|
944
|
-
rule: buildParsedRule(block.id, metadata.value, sections.value)
|
|
945
|
-
};
|
|
1171
|
+
return readContent(diff, start, start + 2, paths, "PLAIN");
|
|
946
1172
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
1173
|
+
function readContent(diff, start, hunksStart, paths, format) {
|
|
1174
|
+
const hunks = readHunks(diff.text, hunksStart);
|
|
1175
|
+
if (hunks.status === "INVALID") {
|
|
1176
|
+
return hunks;
|
|
1177
|
+
}
|
|
1178
|
+
const next = nextSectionIndex(diff.text, hunks.end, format);
|
|
1179
|
+
if (next === null) {
|
|
1180
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
1181
|
+
}
|
|
1182
|
+
const path = paths.newPath ?? paths.oldPath ?? "";
|
|
1183
|
+
const patch = renderPatch(diff, start, hunks.end);
|
|
1184
|
+
return { status: "OK", file: { path, patch }, next };
|
|
949
1185
|
}
|
|
950
|
-
function
|
|
951
|
-
|
|
952
|
-
`).split(`
|
|
1186
|
+
function renderPatch(diff, start, end) {
|
|
1187
|
+
const body = diff.raw.slice(start, end).join(`
|
|
953
1188
|
`);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1189
|
+
return end < diff.raw.length || diff.endsWithNewline ? `${body}
|
|
1190
|
+
` : body;
|
|
1191
|
+
}
|
|
1192
|
+
function nextSectionIndex(text, end, format) {
|
|
1193
|
+
if (end >= text.length || isSectionStart(text, end, format)) {
|
|
1194
|
+
return end;
|
|
1195
|
+
}
|
|
1196
|
+
return isBlankTail(text, end) ? text.length : null;
|
|
1197
|
+
}
|
|
1198
|
+
function readHunks(text, start) {
|
|
1199
|
+
let cursor = start;
|
|
1200
|
+
let hunkCount = 0;
|
|
1201
|
+
while (cursor < text.length && (text[cursor] ?? "").startsWith("@@")) {
|
|
1202
|
+
const hunk = readHunk(text, cursor);
|
|
1203
|
+
if (hunk.status === "INVALID") {
|
|
1204
|
+
return hunk;
|
|
1205
|
+
}
|
|
1206
|
+
cursor = hunk.next;
|
|
1207
|
+
hunkCount += 1;
|
|
971
1208
|
}
|
|
972
|
-
|
|
973
|
-
|
|
1209
|
+
if (hunkCount === 0) {
|
|
1210
|
+
return invalid(cursor >= text.length ? "TRUNCATED_HUNK" : "UNRECOGNIZED_DIFF");
|
|
1211
|
+
}
|
|
1212
|
+
return { status: "OK", end: cursor };
|
|
974
1213
|
}
|
|
975
|
-
function
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
1214
|
+
function readHunk(text, at) {
|
|
1215
|
+
const match = HUNK_HEADER.exec(text[at] ?? "");
|
|
1216
|
+
if (match === null) {
|
|
1217
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
1218
|
+
}
|
|
1219
|
+
let oldLeft = Number(match[2] ?? 1);
|
|
1220
|
+
let newLeft = Number(match[4] ?? 1);
|
|
1221
|
+
let cursor = at + 1;
|
|
1222
|
+
let afterBodyLine = false;
|
|
1223
|
+
while (oldLeft > 0 || newLeft > 0) {
|
|
1224
|
+
if (cursor >= text.length) {
|
|
1225
|
+
return invalid("TRUNCATED_HUNK");
|
|
1226
|
+
}
|
|
1227
|
+
const line = text[cursor] ?? "";
|
|
1228
|
+
if (line.startsWith(NO_NEWLINE_MARKER)) {
|
|
1229
|
+
if (!afterBodyLine) {
|
|
1230
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
1231
|
+
}
|
|
1232
|
+
afterBodyLine = false;
|
|
1233
|
+
cursor += 1;
|
|
979
1234
|
continue;
|
|
980
1235
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (
|
|
986
|
-
|
|
1236
|
+
const marker = line === "" ? " " : line.charAt(0);
|
|
1237
|
+
const consumesOld = marker === " " || marker === "-";
|
|
1238
|
+
const consumesNew = marker === " " || marker === "+";
|
|
1239
|
+
const exceedsCount = consumesOld && oldLeft === 0 || consumesNew && newLeft === 0;
|
|
1240
|
+
if (!(consumesOld || consumesNew) || exceedsCount) {
|
|
1241
|
+
return invalid("UNRECOGNIZED_DIFF");
|
|
987
1242
|
}
|
|
1243
|
+
oldLeft -= consumesOld ? 1 : 0;
|
|
1244
|
+
newLeft -= consumesNew ? 1 : 0;
|
|
1245
|
+
afterBodyLine = true;
|
|
1246
|
+
cursor += 1;
|
|
988
1247
|
}
|
|
989
|
-
|
|
1248
|
+
const closesWithMarker = afterBodyLine && (text[cursor] ?? "").startsWith(NO_NEWLINE_MARKER);
|
|
1249
|
+
return { status: "OK", next: closesWithMarker ? cursor + 1 : cursor };
|
|
990
1250
|
}
|
|
991
|
-
// packages/core/src/
|
|
992
|
-
function
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
const rules = new Map;
|
|
998
|
-
const evaluation = { wholeEvaluations: 0, slicedEvaluations: 0, totalSliceJudgments: 0 };
|
|
999
|
-
let firstTimestamp = null;
|
|
1000
|
-
let lastTimestamp = null;
|
|
1251
|
+
// packages/core/src/feedback/identity.ts
|
|
1252
|
+
function failIdentityKey(identity) {
|
|
1253
|
+
return JSON.stringify([identity.turnId, identity.ruleId]);
|
|
1254
|
+
}
|
|
1255
|
+
function collectRuleFails(records) {
|
|
1256
|
+
const fails = new Map;
|
|
1001
1257
|
for (const record of records) {
|
|
1002
|
-
firstTimestamp = earlier(firstTimestamp, record.timestamp);
|
|
1003
|
-
lastTimestamp = later(lastTimestamp, record.timestamp);
|
|
1004
1258
|
for (const result of record.results) {
|
|
1005
|
-
|
|
1006
|
-
|
|
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);
|
|
1259
|
+
if (result.kind !== "RULE" || result.ruleId === null || result.outcome !== "FAIL") {
|
|
1260
|
+
continue;
|
|
1017
1261
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1262
|
+
const fail = {
|
|
1263
|
+
turnId: record.turnId,
|
|
1264
|
+
ruleId: result.ruleId,
|
|
1265
|
+
timestamp: record.timestamp,
|
|
1266
|
+
violationProbability: result.violationProbability
|
|
1267
|
+
};
|
|
1268
|
+
const key = failIdentityKey(fail);
|
|
1269
|
+
const existing = fails.get(key);
|
|
1270
|
+
if (existing === undefined || fail.timestamp >= existing.timestamp) {
|
|
1271
|
+
fails.set(key, fail);
|
|
1020
1272
|
}
|
|
1021
1273
|
}
|
|
1022
1274
|
}
|
|
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 };
|
|
1275
|
+
return fails;
|
|
1038
1276
|
}
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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;
|
|
1277
|
+
function latestDecisions(feedback) {
|
|
1278
|
+
const decisions = new Map;
|
|
1279
|
+
for (const record of feedback) {
|
|
1280
|
+
decisions.set(failIdentityKey(record), record.decision);
|
|
1056
1281
|
}
|
|
1282
|
+
return decisions;
|
|
1057
1283
|
}
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1284
|
+
function latestFeedbackRecords(feedback) {
|
|
1285
|
+
const latest = new Map;
|
|
1286
|
+
for (const record of feedback) {
|
|
1287
|
+
const key = failIdentityKey(record);
|
|
1288
|
+
latest.delete(key);
|
|
1289
|
+
latest.set(key, record);
|
|
1063
1290
|
}
|
|
1064
|
-
|
|
1291
|
+
return [...latest.values()];
|
|
1065
1292
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return;
|
|
1293
|
+
// packages/core/src/report/compare.ts
|
|
1294
|
+
function compareStrings(left, right) {
|
|
1295
|
+
if (left < right) {
|
|
1296
|
+
return -1;
|
|
1070
1297
|
}
|
|
1071
|
-
|
|
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
|
-
};
|
|
1298
|
+
return left > right ? 1 : 0;
|
|
1083
1299
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1300
|
+
|
|
1301
|
+
// packages/core/src/feedback/pending.ts
|
|
1302
|
+
var FEEDBACK_LIST_LIMIT = 20;
|
|
1303
|
+
function listPendingFails(records, feedback, limit) {
|
|
1304
|
+
const answered = latestDecisions(feedback);
|
|
1305
|
+
const pending = [...collectRuleFails(records)].filter(([key]) => !answered.has(key)).map(([, fail]) => fail).sort(compareNewestFirst);
|
|
1306
|
+
return { items: pending.slice(0, limit), total: pending.length };
|
|
1086
1307
|
}
|
|
1087
|
-
function
|
|
1088
|
-
|
|
1308
|
+
function compareNewestFirst(left, right) {
|
|
1309
|
+
return compareStrings(right.timestamp, left.timestamp) || compareStrings(left.ruleId, right.ruleId) || compareStrings(left.turnId, right.turnId);
|
|
1089
1310
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1311
|
+
// packages/core/src/feedback/resolve.ts
|
|
1312
|
+
function resolveFeedbackTarget(selection, number, records) {
|
|
1313
|
+
if (selection === null) {
|
|
1314
|
+
return { status: "NO_SELECTION" };
|
|
1315
|
+
}
|
|
1316
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
1317
|
+
return { status: "NUMBER_OUT_OF_RANGE" };
|
|
1318
|
+
}
|
|
1319
|
+
const identity = selection.items[number - 1];
|
|
1320
|
+
if (identity === undefined) {
|
|
1321
|
+
return { status: "NUMBER_OUT_OF_RANGE" };
|
|
1322
|
+
}
|
|
1323
|
+
const fail = collectRuleFails(records).get(failIdentityKey(identity));
|
|
1324
|
+
return fail === undefined ? { status: "NO_LONGER_IN_HISTORY" } : { status: "FOUND", fail };
|
|
1092
1325
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
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
|
-
}
|
|
1115
|
-
// packages/core/src/builtins/complexity.ts
|
|
1116
|
-
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
1117
|
-
var COMPLEXITY_GATE_CONFIG = {
|
|
1118
|
-
version: 1,
|
|
1119
|
-
thresholds: {
|
|
1120
|
-
error: { warn: 0.4, fail: 0.7 },
|
|
1121
|
-
warning: { warn: 0.5 }
|
|
1122
|
-
}
|
|
1123
|
-
};
|
|
1124
|
-
var COMPLEXITY_INSTRUCTIONS = "Does this attributed change introduce material complexity that is disproportionate to, or not reasonably necessary for, completing the user's task? Necessary supporting changes, tests, validation, error handling, and complexity explicitly required by the task should not count as unnecessary complexity. Return the probability from 0 to 1 that the change introduces disproportionate or unnecessary complexity.";
|
|
1125
|
-
var COMPLEXITY_DESCRIPTION = "Material complexity introduced by the attributed change that is disproportionate to the problem and not reasonably necessary for completing the task.";
|
|
1126
|
-
var COMPLEXITY_VIOLATION = "The change adds unnecessary abstractions, layers or indirections without proportional gain, new dependencies without clear need, excessive configuration, premature generalization, or structure materially larger than the problem requires.";
|
|
1127
|
-
var COMPLEXITY_ALLOWED = "Complexity explicitly required by the task, and necessary supporting changes, tests, validation, error handling, auxiliary changes, or following an existing codebase abstraction to avoid breaking its pattern.";
|
|
1128
|
-
function buildComplexityQuestion() {
|
|
1129
|
-
return {
|
|
1130
|
-
type: "noul",
|
|
1131
|
-
instructions: COMPLEXITY_INSTRUCTIONS,
|
|
1132
|
-
criteria: {
|
|
1133
|
-
id: COMPLEXITY_CHECK_ID,
|
|
1134
|
-
description: COMPLEXITY_DESCRIPTION,
|
|
1135
|
-
violation: COMPLEXITY_VIOLATION,
|
|
1136
|
-
allowed: COMPLEXITY_ALLOWED
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
function buildComplexityContext(turn, scopedPaths) {
|
|
1141
|
-
return {
|
|
1142
|
-
kind: "BUILT_IN",
|
|
1143
|
-
turnId: turn.id,
|
|
1144
|
-
ruleId: null,
|
|
1145
|
-
checkId: COMPLEXITY_CHECK_ID,
|
|
1146
|
-
severity: "warning",
|
|
1147
|
-
scopedPaths
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// packages/core/src/builtins/scope-creep.ts
|
|
1152
|
-
var SCOPE_CREEP_CHECK_ID = "SCOPE-CREEP";
|
|
1153
|
-
var SCOPE_CREEP_GATE_CONFIG = {
|
|
1154
|
-
version: 1,
|
|
1155
|
-
thresholds: {
|
|
1156
|
-
error: { warn: 0.65, fail: 0.9 },
|
|
1157
|
-
warning: { warn: 0.6 }
|
|
1158
|
-
}
|
|
1159
|
-
};
|
|
1160
|
-
var SCOPE_CREEP_INSTRUCTIONS = "Answer true when the attributed change contains scope creep: material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task. Answer false when every change is requested work, or supporting, testing, or documentation work reasonably necessary to complete the requested work, including immaterial incidental edits. Return the probability from 0 to 1 that the change is scope creep.";
|
|
1161
|
-
var SCOPE_CREEP_DESCRIPTION = "A change is scope creep when it introduces material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete it.";
|
|
1162
|
-
var SCOPE_CREEP_VIOLATION = "The change adds material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task.";
|
|
1163
|
-
var SCOPE_CREEP_ALLOWED = "Changes the task requested, and supporting, testing, and documentation changes reasonably necessary to complete the requested work, including immaterial incidental edits.";
|
|
1164
|
-
function buildScopeCreepQuestion() {
|
|
1165
|
-
return {
|
|
1166
|
-
type: "noul",
|
|
1167
|
-
instructions: SCOPE_CREEP_INSTRUCTIONS,
|
|
1168
|
-
criteria: {
|
|
1169
|
-
id: SCOPE_CREEP_CHECK_ID,
|
|
1170
|
-
description: SCOPE_CREEP_DESCRIPTION,
|
|
1171
|
-
violation: SCOPE_CREEP_VIOLATION,
|
|
1172
|
-
allowed: SCOPE_CREEP_ALLOWED
|
|
1173
|
-
}
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
function buildScopeCreepContext(turn, scopedPaths) {
|
|
1177
|
-
return {
|
|
1178
|
-
kind: "BUILT_IN",
|
|
1179
|
-
turnId: turn.id,
|
|
1180
|
-
ruleId: null,
|
|
1181
|
-
checkId: SCOPE_CREEP_CHECK_ID,
|
|
1182
|
-
severity: "error",
|
|
1183
|
-
scopedPaths
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// packages/core/src/builtins/evaluate-builtins.ts
|
|
1188
|
-
function buildBuiltInBatchRequest(task, evidence) {
|
|
1189
|
-
return {
|
|
1190
|
-
kind: "BUILT_IN_BATCH",
|
|
1191
|
-
task,
|
|
1192
|
-
questions: {
|
|
1193
|
-
[SCOPE_CREEP_ANSWER]: buildScopeCreepQuestion(),
|
|
1194
|
-
[COMPLEXITY_ANSWER]: buildComplexityQuestion()
|
|
1195
|
-
},
|
|
1196
|
-
change: { files: evidence.files, diff: evidence.diff }
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
async function evaluateBuiltIns(input, dependencies) {
|
|
1200
|
-
const selection = selectTurnEvidence(input.turn, input.evidencePolicy);
|
|
1201
|
-
if (selection.status === "SKIPPED") {
|
|
1202
|
-
return {
|
|
1203
|
-
scopeCreep: {
|
|
1204
|
-
...buildScopeCreepContext(input.turn, []),
|
|
1205
|
-
outcome: "SKIPPED",
|
|
1206
|
-
reason: selection.reason
|
|
1207
|
-
},
|
|
1208
|
-
complexity: {
|
|
1209
|
-
...buildComplexityContext(input.turn, []),
|
|
1210
|
-
outcome: "SKIPPED",
|
|
1211
|
-
reason: selection.reason
|
|
1212
|
-
}
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
if (selection.status === "UNAVAILABLE") {
|
|
1216
|
-
return {
|
|
1217
|
-
scopeCreep: {
|
|
1218
|
-
...buildScopeCreepContext(input.turn, []),
|
|
1219
|
-
outcome: "UNAVAILABLE",
|
|
1220
|
-
reason: selection.reason
|
|
1221
|
-
},
|
|
1222
|
-
complexity: {
|
|
1223
|
-
...buildComplexityContext(input.turn, []),
|
|
1224
|
-
outcome: "UNAVAILABLE",
|
|
1225
|
-
reason: selection.reason
|
|
1226
|
-
}
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
const evidence = selection.evidence;
|
|
1230
|
-
const request = buildBuiltInBatchRequest(input.turn.task, evidence);
|
|
1231
|
-
const evaluation = await evaluateBuiltInBatchWithPort(dependencies.jev, request);
|
|
1232
|
-
const scopeCreepContext = buildScopeCreepContext(input.turn, evidence.files);
|
|
1233
|
-
const complexityContext = buildComplexityContext(input.turn, evidence.files);
|
|
1234
|
-
if (evaluation.status === "FAILED") {
|
|
1235
|
-
return {
|
|
1236
|
-
scopeCreep: { ...scopeCreepContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" },
|
|
1237
|
-
complexity: { ...complexityContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" }
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
return {
|
|
1241
|
-
scopeCreep: gateScopeCreep(evaluation.answers.scopeCreep, scopeCreepContext),
|
|
1242
|
-
complexity: gateComplexity(evaluation.answers.complexity, complexityContext)
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
function gateScopeCreep(answer, context) {
|
|
1246
|
-
if (answer.status === "FAILED") {
|
|
1247
|
-
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
1248
|
-
}
|
|
1249
|
-
return {
|
|
1250
|
-
...context,
|
|
1251
|
-
...evaluateGate("error", answer.noul.violationProbability, SCOPE_CREEP_GATE_CONFIG)
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
function gateComplexity(answer, context) {
|
|
1255
|
-
if (answer.status === "FAILED") {
|
|
1256
|
-
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
1257
|
-
}
|
|
1258
|
-
return {
|
|
1259
|
-
...context,
|
|
1260
|
-
...evaluateGate("warning", answer.noul.violationProbability, COMPLEXITY_GATE_CONFIG)
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
// packages/core/src/concurrency/fifo-port.ts
|
|
1264
|
-
function createFifoJevPort(port, config) {
|
|
1265
|
-
const maxConcurrency = config.maxConcurrency;
|
|
1266
|
-
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
1267
|
-
throw new RangeError("maxConcurrency must be a positive integer");
|
|
1268
|
-
}
|
|
1269
|
-
const queue = [];
|
|
1270
|
-
let active = 0;
|
|
1271
|
-
function drain() {
|
|
1272
|
-
while (active < maxConcurrency && queue.length > 0) {
|
|
1273
|
-
const pending = queue.shift();
|
|
1274
|
-
if (pending === undefined) {
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
active += 1;
|
|
1278
|
-
start(pending);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
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
|
-
}
|
|
1294
|
-
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
1295
|
-
active -= 1;
|
|
1296
|
-
pending.settle(result);
|
|
1297
|
-
drain();
|
|
1298
|
-
}, (error) => {
|
|
1299
|
-
active -= 1;
|
|
1300
|
-
pending.fail(error);
|
|
1301
|
-
drain();
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
return {
|
|
1305
|
-
evaluate(request) {
|
|
1306
|
-
return new Promise((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 });
|
|
1314
|
-
drain();
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
};
|
|
1326
|
+
function planFeedbackDecision(target, decision, feedback) {
|
|
1327
|
+
const previous = latestDecisions(feedback).get(failIdentityKey(target)) ?? null;
|
|
1328
|
+
return previous === decision ? { status: "UNCHANGED" } : { status: "RECORD", previous };
|
|
1318
1329
|
}
|
|
1319
1330
|
// packages/core/src/gate/defaults.ts
|
|
1320
1331
|
var DEFAULT_GATE_CONFIG = {
|
|
@@ -1324,31 +1335,32 @@ var DEFAULT_GATE_CONFIG = {
|
|
|
1324
1335
|
warning: { warn: 0.6 }
|
|
1325
1336
|
}
|
|
1326
1337
|
};
|
|
1338
|
+
|
|
1327
1339
|
// packages/core/src/gate/config.ts
|
|
1328
1340
|
function resolveGateConfig(value) {
|
|
1329
1341
|
if (value === null || value === undefined) {
|
|
1330
1342
|
return { status: "VALID", config: DEFAULT_GATE_CONFIG };
|
|
1331
1343
|
}
|
|
1332
1344
|
if (!isRecord(value) || value.version !== 1) {
|
|
1333
|
-
return
|
|
1345
|
+
return invalid2();
|
|
1334
1346
|
}
|
|
1335
1347
|
const thresholds = value.thresholds;
|
|
1336
1348
|
if (!isRecord(thresholds)) {
|
|
1337
|
-
return
|
|
1349
|
+
return invalid2();
|
|
1338
1350
|
}
|
|
1339
1351
|
const error = thresholds.error;
|
|
1340
1352
|
const warning = thresholds.warning;
|
|
1341
1353
|
if (!isRecord(error) || !isRecord(warning)) {
|
|
1342
|
-
return
|
|
1354
|
+
return invalid2();
|
|
1343
1355
|
}
|
|
1344
1356
|
const errorWarn = error.warn;
|
|
1345
1357
|
const errorFail = error.fail;
|
|
1346
1358
|
const warningWarn = warning.warn;
|
|
1347
1359
|
if (!isProbability(errorWarn) || !isProbability(errorFail) || !isProbability(warningWarn)) {
|
|
1348
|
-
return
|
|
1360
|
+
return invalid2();
|
|
1349
1361
|
}
|
|
1350
1362
|
if (errorWarn >= errorFail) {
|
|
1351
|
-
return
|
|
1363
|
+
return invalid2();
|
|
1352
1364
|
}
|
|
1353
1365
|
return {
|
|
1354
1366
|
status: "VALID",
|
|
@@ -1364,102 +1376,223 @@ function resolveGateConfig(value) {
|
|
|
1364
1376
|
function isRecord(value) {
|
|
1365
1377
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1366
1378
|
}
|
|
1367
|
-
function
|
|
1379
|
+
function invalid2() {
|
|
1368
1380
|
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1369
1381
|
}
|
|
1370
1382
|
// packages/core/src/remediation/types.ts
|
|
1371
|
-
var PROPOSER_AGENT_NAME = "jevguard-proposer";
|
|
1372
1383
|
var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
1384
|
+
var MAX_ATTEMPTS_DEFAULT = 2;
|
|
1385
|
+
var MAX_ATTEMPTS_CEILING = 3;
|
|
1386
|
+
var CHAIN_ID_SOURCE = "[A-Za-z0-9._:-]{1,512}";
|
|
1387
|
+
var CHAIN_ID_PATTERN = new RegExp(`^${CHAIN_ID_SOURCE}$`);
|
|
1388
|
+
function isAttemptCount(value) {
|
|
1389
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_ATTEMPTS_CEILING;
|
|
1390
|
+
}
|
|
1391
|
+
var MAX_AGENT_DELIVERIES_PER_SESSION = 3;
|
|
1392
|
+
var PROPOSAL_MAX_TEXT_LENGTH = 32768;
|
|
1393
|
+
function withinSessionLimit(deliveredInSession) {
|
|
1394
|
+
return deliveredInSession < MAX_AGENT_DELIVERIES_PER_SESSION;
|
|
1395
|
+
}
|
|
1373
1396
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
1374
1397
|
autoPropose: true,
|
|
1375
1398
|
proposeOn: ["FAIL"],
|
|
1376
1399
|
model: null
|
|
1377
1400
|
};
|
|
1378
1401
|
|
|
1379
|
-
// packages/core/src/remediation/
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1402
|
+
// packages/core/src/remediation/chain.ts
|
|
1403
|
+
var FNV_PRIME = 16777619;
|
|
1404
|
+
var FNV_OFFSETS = [2166136261, 84696351];
|
|
1405
|
+
function fnv1a(text, offset) {
|
|
1406
|
+
let hash = offset;
|
|
1407
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
1408
|
+
hash = Math.imul(hash ^ text.charCodeAt(index), FNV_PRIME) >>> 0;
|
|
1386
1409
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1410
|
+
return hash.toString(16).padStart(8, "0");
|
|
1411
|
+
}
|
|
1412
|
+
function hashOf(text) {
|
|
1413
|
+
return FNV_OFFSETS.map((offset) => fnv1a(text, offset)).join("");
|
|
1414
|
+
}
|
|
1415
|
+
function idOf(result) {
|
|
1416
|
+
if (result.kind === "RULE")
|
|
1417
|
+
return result.ruleId;
|
|
1418
|
+
return result.kind === "BUILT_IN" ? result.checkId : null;
|
|
1419
|
+
}
|
|
1420
|
+
function sortedUnique(values) {
|
|
1421
|
+
return [...new Set(values)].sort();
|
|
1422
|
+
}
|
|
1423
|
+
function chainFailuresOf(results) {
|
|
1424
|
+
const failures = [];
|
|
1425
|
+
for (const result of results) {
|
|
1426
|
+
if (result.outcome !== "FAIL")
|
|
1427
|
+
continue;
|
|
1428
|
+
if (result.kind === "RULE" && result.severity !== "error")
|
|
1429
|
+
continue;
|
|
1430
|
+
const id = idOf(result);
|
|
1431
|
+
if (id !== null)
|
|
1432
|
+
failures.push({ id, scopedPaths: [...result.scopedPaths] });
|
|
1390
1433
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1434
|
+
return failures;
|
|
1435
|
+
}
|
|
1436
|
+
function failureSignature(failures) {
|
|
1437
|
+
const parts = failures.map((failure) => `${failure.id}:${sortedUnique(failure.scopedPaths).join(",")}`);
|
|
1438
|
+
return hashOf(sortedUnique(parts).join("|"));
|
|
1439
|
+
}
|
|
1440
|
+
function scopesOf(results, ids) {
|
|
1441
|
+
const scopes = {};
|
|
1442
|
+
for (const result of results) {
|
|
1443
|
+
const id = idOf(result);
|
|
1444
|
+
if (id !== null && ids.includes(id))
|
|
1445
|
+
scopes[id] = sortedUnique(result.scopedPaths);
|
|
1395
1446
|
}
|
|
1396
|
-
return
|
|
1397
|
-
evaluationId: input.turn.id,
|
|
1398
|
-
sessionID: input.sessionID,
|
|
1399
|
-
messageID: input.turn.id,
|
|
1400
|
-
model: input.config.model,
|
|
1401
|
-
task: input.turn.task,
|
|
1402
|
-
paths: [...selection.evidence.files],
|
|
1403
|
-
diff: selection.evidence.diff,
|
|
1404
|
-
ruleFindings,
|
|
1405
|
-
builtInFindings
|
|
1406
|
-
};
|
|
1447
|
+
return scopes;
|
|
1407
1448
|
}
|
|
1408
|
-
function
|
|
1409
|
-
|
|
1410
|
-
for (const result of results) {
|
|
1411
|
-
if (!isFailingRule(result)) {
|
|
1412
|
-
continue;
|
|
1413
|
-
}
|
|
1414
|
-
const rule = rulesById.get(result.ruleId);
|
|
1415
|
-
if (rule === undefined) {
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
findings.push({
|
|
1419
|
-
ruleId: rule.id,
|
|
1420
|
-
severity: "error",
|
|
1421
|
-
rule: snapshotRule(rule),
|
|
1422
|
-
probability: result.violationProbability,
|
|
1423
|
-
scopedPaths: [...result.scopedPaths]
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
return findings;
|
|
1449
|
+
function isJudged(result) {
|
|
1450
|
+
return result.outcome === "PASS" || result.outcome === "FAIL";
|
|
1427
1451
|
}
|
|
1428
|
-
function
|
|
1429
|
-
|
|
1430
|
-
for (const result of results) {
|
|
1431
|
-
if (result.kind !== "BUILT_IN" || result.outcome !== "FAIL") {
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
findings.push({
|
|
1435
|
-
checkId: result.checkId,
|
|
1436
|
-
severity: result.severity,
|
|
1437
|
-
probability: result.violationProbability,
|
|
1438
|
-
scopedPaths: [...result.scopedPaths]
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
return findings;
|
|
1452
|
+
function narrowed(before, after) {
|
|
1453
|
+
return before?.some((path) => !after.includes(path)) === true;
|
|
1442
1454
|
}
|
|
1443
|
-
function
|
|
1444
|
-
return
|
|
1455
|
+
function judgedOf(id, results) {
|
|
1456
|
+
return results.filter((result) => idOf(result) === id && isJudged(result));
|
|
1445
1457
|
}
|
|
1446
|
-
function
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1458
|
+
function hasUnverifiedOrigin(state, results) {
|
|
1459
|
+
return state.originIds.some((id) => {
|
|
1460
|
+
const judged = judgedOf(id, results);
|
|
1461
|
+
return judged.length === 0 || judged.some((result) => narrowed(state.scopes[id], sortedUnique(result.scopedPaths)));
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
function chainIdsOf(state, results) {
|
|
1465
|
+
const resolvedIds = state.originIds.filter((id) => {
|
|
1466
|
+
const judged = judgedOf(id, results);
|
|
1467
|
+
return judged.length > 0 && judged.every((result) => result.outcome === "PASS" && !narrowed(state.scopes[id], sortedUnique(result.scopedPaths)));
|
|
1468
|
+
});
|
|
1469
|
+
return { resolvedIds, remainingIds: state.originIds.filter((id) => !resolvedIds.includes(id)) };
|
|
1470
|
+
}
|
|
1471
|
+
function unresolvedIds(state) {
|
|
1472
|
+
return { resolvedIds: [], remainingIds: [...state.originIds] };
|
|
1473
|
+
}
|
|
1474
|
+
function stop(code, input, state, ids = chainIdsOf(state, input.results)) {
|
|
1475
|
+
return { kind: "STOP", code, maxAttempts: input.maxAttempts, state, ids };
|
|
1476
|
+
}
|
|
1477
|
+
function startOf(input, failures) {
|
|
1478
|
+
if (input.request === null || failures.length === 0)
|
|
1479
|
+
return { kind: "NONE" };
|
|
1480
|
+
const signature = failureSignature(failures);
|
|
1481
|
+
const originIds = sortedUnique(failures.map((failure) => failure.id));
|
|
1482
|
+
return {
|
|
1483
|
+
kind: "DELIVER",
|
|
1484
|
+
chainId: input.turnId,
|
|
1485
|
+
attempt: 1,
|
|
1486
|
+
maxAttempts: input.maxAttempts,
|
|
1487
|
+
signature,
|
|
1488
|
+
state: {
|
|
1489
|
+
chainId: input.turnId,
|
|
1490
|
+
originIds,
|
|
1491
|
+
signatures: [signature],
|
|
1492
|
+
turnIds: [input.turnId],
|
|
1493
|
+
attempts: 1,
|
|
1494
|
+
scopes: scopesOf(input.results, originIds)
|
|
1451
1495
|
}
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function continuation(input, state, failures) {
|
|
1499
|
+
if (hasUnverifiedOrigin(state, input.results))
|
|
1500
|
+
return stop("UNVERIFIED", input, state);
|
|
1501
|
+
if (failures.length === 0) {
|
|
1502
|
+
return {
|
|
1503
|
+
kind: "RESOLVED",
|
|
1504
|
+
maxAttempts: input.maxAttempts,
|
|
1505
|
+
state,
|
|
1506
|
+
ids: chainIdsOf(state, input.results)
|
|
1507
|
+
};
|
|
1452
1508
|
}
|
|
1453
|
-
|
|
1509
|
+
if (input.request === null)
|
|
1510
|
+
return stop("UNVERIFIED", input, state);
|
|
1511
|
+
if (failures.some((failure) => !state.originIds.includes(failure.id))) {
|
|
1512
|
+
return stop("REGRESSION_INTRODUCED", input, state);
|
|
1513
|
+
}
|
|
1514
|
+
const signature = failureSignature(failures);
|
|
1515
|
+
if (state.signatures.includes(signature))
|
|
1516
|
+
return stop("REPEATED_FAILURE", input, state);
|
|
1517
|
+
if (state.attempts >= input.maxAttempts)
|
|
1518
|
+
return stop("LIMIT", input, state);
|
|
1519
|
+
const attempt = state.attempts + 1;
|
|
1520
|
+
return {
|
|
1521
|
+
kind: "DELIVER",
|
|
1522
|
+
chainId: state.chainId,
|
|
1523
|
+
attempt,
|
|
1524
|
+
maxAttempts: input.maxAttempts,
|
|
1525
|
+
signature,
|
|
1526
|
+
state: {
|
|
1527
|
+
...state,
|
|
1528
|
+
signatures: [...state.signatures, signature],
|
|
1529
|
+
turnIds: [...state.turnIds, input.turnId],
|
|
1530
|
+
attempts: attempt,
|
|
1531
|
+
scopes: { ...state.scopes, ...scopesOf(input.results, state.originIds) }
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1454
1534
|
}
|
|
1455
|
-
function
|
|
1535
|
+
function unverifiedWithoutState(input, delivery, known) {
|
|
1536
|
+
const state = known ?? {
|
|
1537
|
+
chainId: delivery.chainId,
|
|
1538
|
+
originIds: [],
|
|
1539
|
+
signatures: [],
|
|
1540
|
+
turnIds: [],
|
|
1541
|
+
attempts: Math.min(Math.max(delivery.attempt, 1), MAX_ATTEMPTS_CEILING),
|
|
1542
|
+
scopes: {}
|
|
1543
|
+
};
|
|
1544
|
+
return stop("UNVERIFIED", input, state, unresolvedIds(state));
|
|
1545
|
+
}
|
|
1546
|
+
function decideChainStep(input) {
|
|
1547
|
+
if (input.mode === "observe" || input.delivery === "LEGACY")
|
|
1548
|
+
return { kind: "NONE" };
|
|
1549
|
+
const failures = chainFailuresOf(input.results);
|
|
1550
|
+
const { delivery, chain } = input;
|
|
1551
|
+
if (delivery === null)
|
|
1552
|
+
return startOf(input, failures);
|
|
1553
|
+
if (chain === null || chain.chainId !== delivery.chainId || chain.attempts !== delivery.attempt) {
|
|
1554
|
+
return unverifiedWithoutState(input, delivery, chain);
|
|
1555
|
+
}
|
|
1556
|
+
return continuation(input, chain, failures);
|
|
1557
|
+
}
|
|
1558
|
+
function decideRemediateStep(input) {
|
|
1559
|
+
const { mixed = false, request, ...rest } = input;
|
|
1560
|
+
if (mixed && rest.chain !== null)
|
|
1561
|
+
return decideOrphanStep(rest.chain, rest.maxAttempts);
|
|
1562
|
+
return decideChainStep({ ...rest, mode: "remediate", request: mixed ? null : request });
|
|
1563
|
+
}
|
|
1564
|
+
function decideOrphanStep(state, maxAttempts) {
|
|
1565
|
+
return { kind: "STOP", code: "UNVERIFIED", maxAttempts, state, ids: unresolvedIds(state) };
|
|
1566
|
+
}
|
|
1567
|
+
// packages/core/src/remediation/closures.ts
|
|
1568
|
+
function expiredClosure(state) {
|
|
1569
|
+
return { state, code: "EXPIRED" };
|
|
1570
|
+
}
|
|
1571
|
+
function endRecord(state, maxAttempts, result, ids, turnIds) {
|
|
1456
1572
|
return {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1573
|
+
chainId: state.chainId,
|
|
1574
|
+
turnIds,
|
|
1575
|
+
attempts: state.attempts,
|
|
1576
|
+
maxAttempts,
|
|
1577
|
+
signatures: state.signatures,
|
|
1578
|
+
originIds: state.originIds,
|
|
1579
|
+
resolvedIds: ids.resolvedIds,
|
|
1580
|
+
remainingIds: ids.remainingIds,
|
|
1581
|
+
result
|
|
1461
1582
|
};
|
|
1462
1583
|
}
|
|
1584
|
+
function chainEndOf(step, turnId) {
|
|
1585
|
+
const { state } = step;
|
|
1586
|
+
const turnIds = state.turnIds.includes(turnId) ? state.turnIds : [...state.turnIds, turnId];
|
|
1587
|
+
return endRecord(state, step.maxAttempts, step.kind === "STOP" ? step.code : "RESOLVED", step.ids, turnIds);
|
|
1588
|
+
}
|
|
1589
|
+
async function recordClosures(closures, maxAttempts, sink) {
|
|
1590
|
+
for (const { state, code } of closures) {
|
|
1591
|
+
const entry = endRecord(state, Math.max(maxAttempts, state.attempts), code, unresolvedIds(state), state.turnIds);
|
|
1592
|
+
await sink.stopped(code, entry);
|
|
1593
|
+
await sink.record(entry);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1463
1596
|
// packages/core/src/remediation/config.ts
|
|
1464
1597
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
1465
1598
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
@@ -1469,26 +1602,26 @@ function resolveRemediationConfig(value) {
|
|
|
1469
1602
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
1470
1603
|
}
|
|
1471
1604
|
if (!isRecord2(value)) {
|
|
1472
|
-
return
|
|
1605
|
+
return invalid3();
|
|
1473
1606
|
}
|
|
1474
1607
|
const remediation = value.remediation;
|
|
1475
1608
|
if (remediation === undefined) {
|
|
1476
1609
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
1477
1610
|
}
|
|
1478
1611
|
if (!isRecord2(remediation) || hasUnknownKey(remediation)) {
|
|
1479
|
-
return
|
|
1612
|
+
return invalid3();
|
|
1480
1613
|
}
|
|
1481
1614
|
const autoPropose = remediation.auto_propose ?? DEFAULT_REMEDIATION_CONFIG.autoPropose;
|
|
1482
1615
|
if (typeof autoPropose !== "boolean") {
|
|
1483
|
-
return
|
|
1616
|
+
return invalid3();
|
|
1484
1617
|
}
|
|
1485
1618
|
const proposeOn = readProposeOn(remediation.propose_on);
|
|
1486
1619
|
if (proposeOn === null) {
|
|
1487
|
-
return
|
|
1620
|
+
return invalid3();
|
|
1488
1621
|
}
|
|
1489
1622
|
const model = resolveModel(remediation.model);
|
|
1490
1623
|
if (model === INVALID_MODEL) {
|
|
1491
|
-
return
|
|
1624
|
+
return invalid3();
|
|
1492
1625
|
}
|
|
1493
1626
|
return valid({ autoPropose, proposeOn, model });
|
|
1494
1627
|
}
|
|
@@ -1536,9 +1669,76 @@ function isRecord2(value) {
|
|
|
1536
1669
|
function valid(config) {
|
|
1537
1670
|
return { status: "VALID", config };
|
|
1538
1671
|
}
|
|
1539
|
-
function
|
|
1672
|
+
function invalid3() {
|
|
1540
1673
|
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1541
1674
|
}
|
|
1675
|
+
// packages/core/src/remediation/delivery-findings.ts
|
|
1676
|
+
function deliveryFindingsOf(request) {
|
|
1677
|
+
return [
|
|
1678
|
+
...request.ruleFindings.map((finding) => ({
|
|
1679
|
+
id: finding.ruleId,
|
|
1680
|
+
severity: finding.severity,
|
|
1681
|
+
probability: finding.probability,
|
|
1682
|
+
paths: finding.scopedPaths,
|
|
1683
|
+
violation: finding.rule.violation,
|
|
1684
|
+
allowed: finding.rule.allowed
|
|
1685
|
+
})),
|
|
1686
|
+
...request.builtInFindings.map((finding) => ({
|
|
1687
|
+
id: finding.checkId,
|
|
1688
|
+
severity: finding.severity,
|
|
1689
|
+
probability: finding.probability,
|
|
1690
|
+
paths: finding.scopedPaths,
|
|
1691
|
+
violation: null,
|
|
1692
|
+
allowed: null
|
|
1693
|
+
}))
|
|
1694
|
+
];
|
|
1695
|
+
}
|
|
1696
|
+
// packages/core/src/remediation/delivery-frame.ts
|
|
1697
|
+
var AUTO_DELIVERY_MARKER = "[JevGuard remediation proposal \xB7 automatic delivery";
|
|
1698
|
+
var AGENT_NOTICE = "Sent by the JevGuard plugin, not typed by the user. Strategy-only proposal generated without tools for the FAIL findings of the previous work unit. Apply it only if it is consistent with the user's instructions; do not change tests, configuration or dependencies without explicit user confirmation.";
|
|
1699
|
+
var PROPOSAL_START = "--- proposal start ---";
|
|
1700
|
+
var PROPOSAL_END = "--- proposal end ---";
|
|
1701
|
+
var CONSTRAINTS = "Constraints: touch only the listed paths; do not change tests, configuration, dependencies or .jev/; do not commit.";
|
|
1702
|
+
var STOP_CLAUSE = "If you believe a finding is wrong, say so and stop instead of forcing a change.";
|
|
1703
|
+
var CONTROL_CHARACTERS = /(?![\n\t])\p{Cc}/gu;
|
|
1704
|
+
var FINDING_TEXT_MAX = 1000;
|
|
1705
|
+
var MARK = new RegExp(`\\[JevGuard remediation proposal \xB7 automatic delivery(?: \xB7 [^\\n\\]\xB7]*? \xB7 attempt ([1-9][0-9]*)/([1-9][0-9]*) \xB7 chain (${CHAIN_ID_SOURCE}))?\\]`);
|
|
1706
|
+
function clean(text) {
|
|
1707
|
+
return text.replace(CONTROL_CHARACTERS, "");
|
|
1708
|
+
}
|
|
1709
|
+
function oneLine(text) {
|
|
1710
|
+
return clean(text).replace(/\s+/g, " ").trim().slice(0, FINDING_TEXT_MAX);
|
|
1711
|
+
}
|
|
1712
|
+
function findingLines(finding) {
|
|
1713
|
+
const head = `- ${oneLine(finding.id)} (${oneLine(finding.severity)}, p=${finding.probability.toFixed(2)}) in ${finding.paths.map(oneLine).join(", ")}`;
|
|
1714
|
+
const lines = [head];
|
|
1715
|
+
if (finding.violation !== null && finding.violation !== "") {
|
|
1716
|
+
lines.push(` Violation: ${oneLine(finding.violation)}`);
|
|
1717
|
+
}
|
|
1718
|
+
if (finding.allowed !== null && finding.allowed !== "") {
|
|
1719
|
+
lines.push(` Allowed: ${oneLine(finding.allowed)}`);
|
|
1720
|
+
}
|
|
1721
|
+
return lines;
|
|
1722
|
+
}
|
|
1723
|
+
function formatAgentDelivery(input) {
|
|
1724
|
+
const strategy = clean(input.strategyText).split(`
|
|
1725
|
+
`).filter((line) => line.trim() !== PROPOSAL_START && line.trim() !== PROPOSAL_END);
|
|
1726
|
+
return [
|
|
1727
|
+
`${AUTO_DELIVERY_MARKER} \xB7 ${oneLine(input.unit)} \xB7 attempt ${input.attempt}/${input.maxAttempts} \xB7 chain ${input.chainId}]`,
|
|
1728
|
+
AGENT_NOTICE,
|
|
1729
|
+
"",
|
|
1730
|
+
"Findings:",
|
|
1731
|
+
...input.findings.flatMap(findingLines),
|
|
1732
|
+
"",
|
|
1733
|
+
PROPOSAL_START,
|
|
1734
|
+
...strategy,
|
|
1735
|
+
PROPOSAL_END,
|
|
1736
|
+
"",
|
|
1737
|
+
CONSTRAINTS,
|
|
1738
|
+
STOP_CLAUSE
|
|
1739
|
+
].join(`
|
|
1740
|
+
`);
|
|
1741
|
+
}
|
|
1542
1742
|
// packages/core/src/remediation/prompt.ts
|
|
1543
1743
|
var PROPOSER_AGENT_PROMPT = [
|
|
1544
1744
|
"JevGuard remediation proposer.",
|
|
@@ -1609,6 +1809,90 @@ function section(label, value) {
|
|
|
1609
1809
|
${value}
|
|
1610
1810
|
`;
|
|
1611
1811
|
}
|
|
1812
|
+
// packages/core/src/remediation/request.ts
|
|
1813
|
+
function buildProposalRequest(input) {
|
|
1814
|
+
if (!input.config.autoPropose || !input.config.proposeOn.includes("FAIL")) {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
if (input.turn.task.length > REMEDIATION_MAX_TASK_LENGTH) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
const selection = selectTurnEvidence(input.turn, input.evidencePolicy ?? DEFAULT_EVIDENCE_POLICY);
|
|
1821
|
+
if (selection.status !== "SELECTED") {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
const ruleFindings = collectRuleFindings(input.results, indexParsedRules(input.rules));
|
|
1825
|
+
const builtInFindings = collectBuiltInFindings(input.results);
|
|
1826
|
+
if (ruleFindings.length === 0 && builtInFindings.length === 0) {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
return {
|
|
1830
|
+
evaluationId: input.turn.id,
|
|
1831
|
+
sessionID: input.sessionID,
|
|
1832
|
+
messageID: input.turn.id,
|
|
1833
|
+
model: input.config.model,
|
|
1834
|
+
task: input.turn.task,
|
|
1835
|
+
paths: [...selection.evidence.files],
|
|
1836
|
+
diff: selection.evidence.diff,
|
|
1837
|
+
ruleFindings,
|
|
1838
|
+
builtInFindings
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function collectRuleFindings(results, rulesById) {
|
|
1842
|
+
const findings = [];
|
|
1843
|
+
for (const result of results) {
|
|
1844
|
+
if (!isFailingRule(result)) {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const rule = rulesById.get(result.ruleId);
|
|
1848
|
+
if (rule === undefined) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
findings.push({
|
|
1852
|
+
ruleId: rule.id,
|
|
1853
|
+
severity: "error",
|
|
1854
|
+
rule: snapshotRule(rule),
|
|
1855
|
+
probability: result.violationProbability,
|
|
1856
|
+
scopedPaths: [...result.scopedPaths]
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
return findings;
|
|
1860
|
+
}
|
|
1861
|
+
function collectBuiltInFindings(results) {
|
|
1862
|
+
const findings = [];
|
|
1863
|
+
for (const result of results) {
|
|
1864
|
+
if (result.kind !== "BUILT_IN" || result.outcome !== "FAIL") {
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
findings.push({
|
|
1868
|
+
checkId: result.checkId,
|
|
1869
|
+
severity: result.severity,
|
|
1870
|
+
probability: result.violationProbability,
|
|
1871
|
+
scopedPaths: [...result.scopedPaths]
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return findings;
|
|
1875
|
+
}
|
|
1876
|
+
function isFailingRule(result) {
|
|
1877
|
+
return result.kind === "RULE" && result.outcome === "FAIL" && result.severity === "error" && result.ruleId !== null;
|
|
1878
|
+
}
|
|
1879
|
+
function indexParsedRules(rules) {
|
|
1880
|
+
const byId = new Map;
|
|
1881
|
+
for (const candidate of rules) {
|
|
1882
|
+
if (candidate.status === "PARSED") {
|
|
1883
|
+
byId.set(candidate.rule.id, candidate.rule);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return byId;
|
|
1887
|
+
}
|
|
1888
|
+
function snapshotRule(rule) {
|
|
1889
|
+
return {
|
|
1890
|
+
id: rule.id,
|
|
1891
|
+
description: rule.description,
|
|
1892
|
+
violation: rule.violation,
|
|
1893
|
+
allowed: rule.allowed ?? ""
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1612
1896
|
// packages/core/src/remediation/store.ts
|
|
1613
1897
|
function createRemediationProposalStore() {
|
|
1614
1898
|
const claimed = new Set;
|
|
@@ -1631,6 +1915,561 @@ function createRemediationProposalStore() {
|
|
|
1631
1915
|
}
|
|
1632
1916
|
};
|
|
1633
1917
|
}
|
|
1918
|
+
// packages/core/src/report/aggregate.ts
|
|
1919
|
+
function aggregateReviewHistory(records, skippedLineCount, feedback) {
|
|
1920
|
+
const outcomes = emptyAccumulator();
|
|
1921
|
+
const unavailableReasons = new Map;
|
|
1922
|
+
const skippedReasons = new Map;
|
|
1923
|
+
const builtIns = new Map;
|
|
1924
|
+
const rules = new Map;
|
|
1925
|
+
const evaluation = { wholeEvaluations: 0, slicedEvaluations: 0, totalSliceJudgments: 0 };
|
|
1926
|
+
let firstTimestamp = null;
|
|
1927
|
+
let lastTimestamp = null;
|
|
1928
|
+
for (const record of records) {
|
|
1929
|
+
firstTimestamp = earlier(firstTimestamp, record.timestamp);
|
|
1930
|
+
lastTimestamp = later(lastTimestamp, record.timestamp);
|
|
1931
|
+
for (const result of record.results) {
|
|
1932
|
+
countOutcome(outcomes, result.outcome);
|
|
1933
|
+
if (result.outcome === "UNAVAILABLE") {
|
|
1934
|
+
bump(unavailableReasons, result.reason);
|
|
1935
|
+
}
|
|
1936
|
+
if (result.outcome === "SKIPPED") {
|
|
1937
|
+
bump(skippedReasons, result.reason);
|
|
1938
|
+
}
|
|
1939
|
+
if (result.kind === "BUILT_IN") {
|
|
1940
|
+
bump(builtIns, result.checkId);
|
|
1941
|
+
}
|
|
1942
|
+
if (result.kind === "RULE" && result.ruleId !== null) {
|
|
1943
|
+
countRule(rules, result.ruleId, result.outcome, result.evidence);
|
|
1944
|
+
}
|
|
1945
|
+
if (result.kind === "RULE" && result.evidence !== undefined) {
|
|
1946
|
+
countEvidence(evaluation, result.evidence.mode, result.evidence.sliceJudgments.length);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
entryCount: records.length,
|
|
1952
|
+
skippedLineCount,
|
|
1953
|
+
firstTimestamp,
|
|
1954
|
+
lastTimestamp,
|
|
1955
|
+
outcomes: finalizeAccumulator(outcomes),
|
|
1956
|
+
unavailableReasons: sortedReasons(unavailableReasons),
|
|
1957
|
+
skippedReasons: sortedReasons(skippedReasons),
|
|
1958
|
+
builtIns: sortedBuiltIns(builtIns),
|
|
1959
|
+
rules: sortedRules(rules, calibrateRules(records, feedback)),
|
|
1960
|
+
evaluation: finalizeEvaluation(evaluation)
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function emptyAccumulator() {
|
|
1964
|
+
return { pass: 0, warn: 0, fail: 0, unavailable: 0, skipped: 0 };
|
|
1965
|
+
}
|
|
1966
|
+
function emptyCalibration() {
|
|
1967
|
+
return { right: 0, wrong: 0, unanswered: 0 };
|
|
1968
|
+
}
|
|
1969
|
+
function countOutcome(counts, outcome) {
|
|
1970
|
+
switch (outcome) {
|
|
1971
|
+
case "PASS":
|
|
1972
|
+
counts.pass += 1;
|
|
1973
|
+
return;
|
|
1974
|
+
case "WARN":
|
|
1975
|
+
counts.warn += 1;
|
|
1976
|
+
return;
|
|
1977
|
+
case "FAIL":
|
|
1978
|
+
counts.fail += 1;
|
|
1979
|
+
return;
|
|
1980
|
+
case "UNAVAILABLE":
|
|
1981
|
+
counts.unavailable += 1;
|
|
1982
|
+
return;
|
|
1983
|
+
case "SKIPPED":
|
|
1984
|
+
counts.skipped += 1;
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function countRule(rules, ruleId, outcome, evidence) {
|
|
1989
|
+
const accumulator = rules.get(ruleId) ?? { counts: emptyAccumulator(), sliced: 0 };
|
|
1990
|
+
countOutcome(accumulator.counts, outcome);
|
|
1991
|
+
if (evidence?.mode === "SLICED") {
|
|
1992
|
+
accumulator.sliced += 1;
|
|
1993
|
+
}
|
|
1994
|
+
rules.set(ruleId, accumulator);
|
|
1995
|
+
}
|
|
1996
|
+
function countEvidence(coverage, mode, sliceJudgmentCount) {
|
|
1997
|
+
if (mode === "WHOLE") {
|
|
1998
|
+
coverage.wholeEvaluations += 1;
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
coverage.slicedEvaluations += 1;
|
|
2002
|
+
coverage.totalSliceJudgments += sliceJudgmentCount;
|
|
2003
|
+
}
|
|
2004
|
+
function finalizeAccumulator(counts) {
|
|
2005
|
+
return {
|
|
2006
|
+
pass: counts.pass,
|
|
2007
|
+
warn: counts.warn,
|
|
2008
|
+
fail: counts.fail,
|
|
2009
|
+
unavailable: counts.unavailable,
|
|
2010
|
+
skipped: counts.skipped,
|
|
2011
|
+
total: counts.pass + counts.warn + counts.fail + counts.unavailable + counts.skipped
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function finalizeEvaluation(coverage) {
|
|
2015
|
+
return { ...coverage };
|
|
2016
|
+
}
|
|
2017
|
+
function bump(map, key) {
|
|
2018
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
2019
|
+
}
|
|
2020
|
+
function sortedReasons(map) {
|
|
2021
|
+
return [...map.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareStrings(left.reason, right.reason));
|
|
2022
|
+
}
|
|
2023
|
+
function sortedBuiltIns(map) {
|
|
2024
|
+
return [...map.entries()].map(([checkId, count]) => ({ checkId, count })).sort((left, right) => compareStrings(left.checkId, right.checkId));
|
|
2025
|
+
}
|
|
2026
|
+
function calibrateRules(records, feedback) {
|
|
2027
|
+
const decisions = latestDecisions(feedback);
|
|
2028
|
+
const calibrations = new Map;
|
|
2029
|
+
for (const [key, fail] of collectRuleFails(records)) {
|
|
2030
|
+
const accumulator = calibrations.get(fail.ruleId) ?? emptyCalibration();
|
|
2031
|
+
const decision = decisions.get(key);
|
|
2032
|
+
if (decision === "RIGHT") {
|
|
2033
|
+
accumulator.right += 1;
|
|
2034
|
+
} else if (decision === "WRONG") {
|
|
2035
|
+
accumulator.wrong += 1;
|
|
2036
|
+
} else {
|
|
2037
|
+
accumulator.unanswered += 1;
|
|
2038
|
+
}
|
|
2039
|
+
calibrations.set(fail.ruleId, accumulator);
|
|
2040
|
+
}
|
|
2041
|
+
return calibrations;
|
|
2042
|
+
}
|
|
2043
|
+
function finalizeCalibration(accumulator) {
|
|
2044
|
+
const { right, wrong, unanswered } = accumulator ?? emptyCalibration();
|
|
2045
|
+
const answered = right + wrong;
|
|
2046
|
+
return { right, wrong, unanswered, precision: answered === 0 ? null : right / answered };
|
|
2047
|
+
}
|
|
2048
|
+
function sortedRules(map, calibrations) {
|
|
2049
|
+
return [...map.entries()].map(([ruleId, accumulator]) => ({
|
|
2050
|
+
ruleId,
|
|
2051
|
+
counts: finalizeAccumulator(accumulator.counts),
|
|
2052
|
+
slicedRuleCount: accumulator.sliced,
|
|
2053
|
+
calibration: finalizeCalibration(calibrations.get(ruleId))
|
|
2054
|
+
})).sort((left, right) => compareStrings(left.ruleId, right.ruleId));
|
|
2055
|
+
}
|
|
2056
|
+
function earlier(current, candidate) {
|
|
2057
|
+
return current === null || candidate < current ? candidate : current;
|
|
2058
|
+
}
|
|
2059
|
+
function later(current, candidate) {
|
|
2060
|
+
return current === null || candidate > current ? candidate : current;
|
|
2061
|
+
}
|
|
2062
|
+
// packages/core/src/review/policy.ts
|
|
2063
|
+
async function loadPolicy(policy) {
|
|
2064
|
+
try {
|
|
2065
|
+
return await policy.load();
|
|
2066
|
+
} catch {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function readPolicyConfig(loaded, parseConfig) {
|
|
2071
|
+
if (loaded === null || loaded.status === "FAILED") {
|
|
2072
|
+
return invalidPolicyConfig();
|
|
2073
|
+
}
|
|
2074
|
+
return diagnosePolicyConfig(loaded.source.config, parseConfig).config;
|
|
2075
|
+
}
|
|
2076
|
+
function diagnosePolicyConfig(config, parseConfig) {
|
|
2077
|
+
const parsed = config === null ? null : parseConfig(config);
|
|
2078
|
+
if (parsed !== null && parsed.status !== "PARSED") {
|
|
2079
|
+
return { code: "PARSE_INVALID", config: invalidPolicyConfig() };
|
|
2080
|
+
}
|
|
2081
|
+
const value = parsed === null ? null : parsed.value;
|
|
2082
|
+
const gate = resolveGateConfig(value);
|
|
2083
|
+
const remediation = resolveRemediationConfig(value);
|
|
2084
|
+
if (gate.status === "INVALID") {
|
|
2085
|
+
return { code: "GATE_INVALID", config: invalidPolicyConfig() };
|
|
2086
|
+
}
|
|
2087
|
+
if (remediation.status === "INVALID") {
|
|
2088
|
+
return { code: "REMEDIATION_INVALID", config: invalidPolicyConfig() };
|
|
2089
|
+
}
|
|
2090
|
+
return { code: config === null ? "DEFAULTS" : "VALID", config: { gate, remediation } };
|
|
2091
|
+
}
|
|
2092
|
+
function invalidPolicyConfig() {
|
|
2093
|
+
return {
|
|
2094
|
+
gate: { status: "INVALID", reason: "INVALID_CONFIG" },
|
|
2095
|
+
remediation: { status: "INVALID", reason: "INVALID_CONFIG" }
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// packages/core/src/rules/headings.ts
|
|
2100
|
+
var RULE_HEADING = /^##\s+(.*)$/;
|
|
2101
|
+
var SECTION_HEADING = /^###\s+(.*)$/;
|
|
2102
|
+
function matchRuleHeading(line) {
|
|
2103
|
+
const match = RULE_HEADING.exec(line);
|
|
2104
|
+
if (match === null) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
return (match[1] ?? "").trim();
|
|
2108
|
+
}
|
|
2109
|
+
function matchSectionHeading(line) {
|
|
2110
|
+
const match = SECTION_HEADING.exec(line);
|
|
2111
|
+
if (match === null) {
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
return (match[1] ?? "").trim();
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// packages/core/src/rules/metadata.ts
|
|
2118
|
+
var METADATA_ENTRY = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
2119
|
+
function parseRuleMetadata(lines) {
|
|
2120
|
+
let severity = null;
|
|
2121
|
+
let evidence = null;
|
|
2122
|
+
let scopeDeclared = false;
|
|
2123
|
+
const includePatterns = [];
|
|
2124
|
+
const excludePatterns = [];
|
|
2125
|
+
const seenKeys = new Set;
|
|
2126
|
+
for (const line of collectMetadataLines(lines)) {
|
|
2127
|
+
const entry = parseMetadataEntry(line);
|
|
2128
|
+
if (entry === null) {
|
|
2129
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
2130
|
+
}
|
|
2131
|
+
if (entry.key === "scope") {
|
|
2132
|
+
const pattern = parseScopePattern(entry.value);
|
|
2133
|
+
if (pattern === null) {
|
|
2134
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
2135
|
+
}
|
|
2136
|
+
scopeDeclared = true;
|
|
2137
|
+
if (pattern.exclude) {
|
|
2138
|
+
excludePatterns.push(pattern.pattern);
|
|
2139
|
+
} else {
|
|
2140
|
+
includePatterns.push(pattern.pattern);
|
|
2141
|
+
}
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
if (seenKeys.has(entry.key)) {
|
|
2145
|
+
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
2146
|
+
}
|
|
2147
|
+
seenKeys.add(entry.key);
|
|
2148
|
+
if (entry.key === "severity") {
|
|
2149
|
+
const parsed = parseSeverity(entry.value);
|
|
2150
|
+
if (parsed === null) {
|
|
2151
|
+
return { ok: false, code: "INVALID_SEVERITY" };
|
|
2152
|
+
}
|
|
2153
|
+
severity = parsed;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (entry.key === "evidence") {
|
|
2157
|
+
const parsed = parseEvidenceClass(entry.value);
|
|
2158
|
+
if (parsed === null) {
|
|
2159
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
2160
|
+
}
|
|
2161
|
+
evidence = parsed;
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
2165
|
+
}
|
|
2166
|
+
if (severity === null) {
|
|
2167
|
+
return { ok: false, code: "MISSING_SEVERITY" };
|
|
2168
|
+
}
|
|
2169
|
+
if (scopeDeclared && includePatterns.length === 0) {
|
|
2170
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
2171
|
+
}
|
|
2172
|
+
const scope = scopeDeclared ? { include: includePatterns, exclude: excludePatterns } : null;
|
|
2173
|
+
return { ok: true, value: { severity, scope, evidence: evidence ?? "code" } };
|
|
2174
|
+
}
|
|
2175
|
+
function collectMetadataLines(lines) {
|
|
2176
|
+
const collected = [];
|
|
2177
|
+
for (const line of lines) {
|
|
2178
|
+
if (matchSectionHeading(line) !== null) {
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
if (line.trim() !== "") {
|
|
2182
|
+
collected.push(line);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return collected;
|
|
2186
|
+
}
|
|
2187
|
+
function parseMetadataEntry(line) {
|
|
2188
|
+
const match = METADATA_ENTRY.exec(line);
|
|
2189
|
+
if (match === null) {
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2192
|
+
return { key: match[1] ?? "", value: (match[2] ?? "").trim() };
|
|
2193
|
+
}
|
|
2194
|
+
function parseSeverity(value) {
|
|
2195
|
+
switch (value) {
|
|
2196
|
+
case "error":
|
|
2197
|
+
case "warning":
|
|
2198
|
+
return value;
|
|
2199
|
+
default:
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function parseEvidenceClass(value) {
|
|
2204
|
+
switch (value) {
|
|
2205
|
+
case "code":
|
|
2206
|
+
case "docs":
|
|
2207
|
+
case "any":
|
|
2208
|
+
return value;
|
|
2209
|
+
default:
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
function parseScopePattern(value) {
|
|
2214
|
+
if (value === "") {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
if (value.startsWith("!")) {
|
|
2218
|
+
const pattern = value.slice(1).trim();
|
|
2219
|
+
return pattern === "" ? null : { pattern, exclude: true };
|
|
2220
|
+
}
|
|
2221
|
+
return { pattern: value, exclude: false };
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// packages/core/src/rules/sections.ts
|
|
2225
|
+
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
2226
|
+
function parseRuleSections(lines) {
|
|
2227
|
+
const collected = collectSections(lines);
|
|
2228
|
+
if (!collected.ok) {
|
|
2229
|
+
return collected;
|
|
2230
|
+
}
|
|
2231
|
+
return validateSections(collected.value);
|
|
2232
|
+
}
|
|
2233
|
+
function collectSections(lines) {
|
|
2234
|
+
const sections = new Map;
|
|
2235
|
+
let current = null;
|
|
2236
|
+
let content = [];
|
|
2237
|
+
for (const line of lines) {
|
|
2238
|
+
const heading = matchSectionHeading(line);
|
|
2239
|
+
if (heading === null) {
|
|
2240
|
+
if (current !== null) {
|
|
2241
|
+
content.push(line);
|
|
2242
|
+
}
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (current !== null) {
|
|
2246
|
+
sections.set(current, joinContent(content));
|
|
2247
|
+
}
|
|
2248
|
+
if (!SECTION_NAMES.includes(heading)) {
|
|
2249
|
+
return { ok: false, code: "UNKNOWN_SECTION" };
|
|
2250
|
+
}
|
|
2251
|
+
if (sections.has(heading)) {
|
|
2252
|
+
return { ok: false, code: "DUPLICATE_SECTION" };
|
|
2253
|
+
}
|
|
2254
|
+
current = heading;
|
|
2255
|
+
content = [];
|
|
2256
|
+
}
|
|
2257
|
+
if (current !== null) {
|
|
2258
|
+
sections.set(current, joinContent(content));
|
|
2259
|
+
}
|
|
2260
|
+
return { ok: true, value: sections };
|
|
2261
|
+
}
|
|
2262
|
+
function validateSections(sections) {
|
|
2263
|
+
const rule = sections.get("Rule");
|
|
2264
|
+
const violation = sections.get("Violation");
|
|
2265
|
+
const allowed = sections.get("Allowed") ?? null;
|
|
2266
|
+
if (rule === undefined) {
|
|
2267
|
+
return { ok: false, code: "MISSING_RULE" };
|
|
2268
|
+
}
|
|
2269
|
+
if (violation === undefined) {
|
|
2270
|
+
return { ok: false, code: "MISSING_VIOLATION" };
|
|
2271
|
+
}
|
|
2272
|
+
if (rule === "") {
|
|
2273
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
2274
|
+
}
|
|
2275
|
+
if (violation === "") {
|
|
2276
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
2277
|
+
}
|
|
2278
|
+
if (allowed === "") {
|
|
2279
|
+
return { ok: false, code: "EMPTY_ALLOWED" };
|
|
2280
|
+
}
|
|
2281
|
+
if (allowed !== null && allowed === violation) {
|
|
2282
|
+
return { ok: false, code: "ALLOWED_EQUALS_VIOLATION" };
|
|
2283
|
+
}
|
|
2284
|
+
return { ok: true, value: { rule, violation, allowed } };
|
|
2285
|
+
}
|
|
2286
|
+
function joinContent(lines) {
|
|
2287
|
+
return lines.join(`
|
|
2288
|
+
`).trim();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// packages/core/src/rules/blocks.ts
|
|
2292
|
+
var RULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
2293
|
+
function isValidRuleId(id) {
|
|
2294
|
+
return RULE_ID.test(id);
|
|
2295
|
+
}
|
|
2296
|
+
function collectRuleBlocks(text) {
|
|
2297
|
+
const blocks = [];
|
|
2298
|
+
let current = null;
|
|
2299
|
+
for (const line of splitLines2(text)) {
|
|
2300
|
+
const id = matchRuleHeading(line);
|
|
2301
|
+
if (id !== null) {
|
|
2302
|
+
current = { id, body: [] };
|
|
2303
|
+
blocks.push(current);
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (current !== null) {
|
|
2307
|
+
current.body.push(line);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return blocks;
|
|
2311
|
+
}
|
|
2312
|
+
function parseRuleBlock(block, duplicate) {
|
|
2313
|
+
if (!isValidRuleId(block.id)) {
|
|
2314
|
+
return ruleCandidateFailure("INVALID_ID", null);
|
|
2315
|
+
}
|
|
2316
|
+
if (duplicate) {
|
|
2317
|
+
return ruleCandidateFailure("DUPLICATE_ID", block.id);
|
|
2318
|
+
}
|
|
2319
|
+
const metadata = parseRuleMetadata(block.body);
|
|
2320
|
+
if (!metadata.ok) {
|
|
2321
|
+
return ruleCandidateFailure(metadata.code, block.id);
|
|
2322
|
+
}
|
|
2323
|
+
const sections = parseRuleSections(block.body);
|
|
2324
|
+
if (!sections.ok) {
|
|
2325
|
+
return ruleCandidateFailure(sections.code, block.id);
|
|
2326
|
+
}
|
|
2327
|
+
return {
|
|
2328
|
+
status: "PARSED",
|
|
2329
|
+
rule: buildParsedRule(block.id, metadata.value, sections.value)
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function ruleCandidateFailure(code, ruleId) {
|
|
2333
|
+
return { status: "INVALID", reason: "INVALID_RULE", code, ruleId };
|
|
2334
|
+
}
|
|
2335
|
+
function splitLines2(text) {
|
|
2336
|
+
return text.replace(/\r\n?/g, `
|
|
2337
|
+
`).split(`
|
|
2338
|
+
`);
|
|
2339
|
+
}
|
|
2340
|
+
function buildParsedRule(id, metadata, sections) {
|
|
2341
|
+
return {
|
|
2342
|
+
id,
|
|
2343
|
+
severity: metadata.severity,
|
|
2344
|
+
scope: metadata.scope,
|
|
2345
|
+
evidence: metadata.evidence,
|
|
2346
|
+
description: sections.rule,
|
|
2347
|
+
violation: sections.violation,
|
|
2348
|
+
allowed: sections.allowed
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// packages/core/src/rules/parse-rules.ts
|
|
2353
|
+
function parseRules(text) {
|
|
2354
|
+
const blocks = collectRuleBlocks(text);
|
|
2355
|
+
if (blocks.length === 0) {
|
|
2356
|
+
return [ruleCandidateFailure("MISSING_ID", null)];
|
|
2357
|
+
}
|
|
2358
|
+
const duplicates = findDuplicateIds(blocks.map((block) => block.id));
|
|
2359
|
+
return blocks.map((block) => parseRuleBlock(block, duplicates.has(block.id)));
|
|
2360
|
+
}
|
|
2361
|
+
function findDuplicateIds(ids) {
|
|
2362
|
+
const counts = new Map;
|
|
2363
|
+
for (const id of ids) {
|
|
2364
|
+
if (!isValidRuleId(id)) {
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
2368
|
+
}
|
|
2369
|
+
const duplicates = new Set;
|
|
2370
|
+
for (const [id, count] of counts) {
|
|
2371
|
+
if (count > 1) {
|
|
2372
|
+
duplicates.add(id);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return duplicates;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// packages/core/src/review/unavailable.ts
|
|
2379
|
+
function reviewLevelUnavailable(turnId, reason) {
|
|
2380
|
+
return {
|
|
2381
|
+
kind: "REVIEW",
|
|
2382
|
+
turnId,
|
|
2383
|
+
ruleId: null,
|
|
2384
|
+
severity: null,
|
|
2385
|
+
scopedPaths: [],
|
|
2386
|
+
outcome: "UNAVAILABLE",
|
|
2387
|
+
reason
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
function unavailableReview(turnId, reason) {
|
|
2391
|
+
return aggregateReview(turnId, [reviewLevelUnavailable(turnId, reason)]);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// packages/core/src/review/rule-lane.ts
|
|
2395
|
+
function resolveRuleCandidates(turnId, loaded) {
|
|
2396
|
+
if (loaded === null) {
|
|
2397
|
+
return unavailableResolution(turnId, "INVALID_RULE");
|
|
2398
|
+
}
|
|
2399
|
+
if (loaded.status === "FAILED") {
|
|
2400
|
+
const reason = loaded.reason === "CONFIG_READ_FAILURE" ? "INVALID_CONFIG" : "INVALID_RULE";
|
|
2401
|
+
return unavailableResolution(turnId, reason);
|
|
2402
|
+
}
|
|
2403
|
+
const rules = loaded.source.rules;
|
|
2404
|
+
if (rules === null) {
|
|
2405
|
+
return unavailableResolution(turnId, "INVALID_RULE");
|
|
2406
|
+
}
|
|
2407
|
+
return { status: "RULES", rules: parseRules(rules) };
|
|
2408
|
+
}
|
|
2409
|
+
async function evaluateRuleCandidates(turn, rules, gate, dependencies) {
|
|
2410
|
+
const review = await evaluateRules({
|
|
2411
|
+
turn,
|
|
2412
|
+
rules,
|
|
2413
|
+
gateConfig: gate,
|
|
2414
|
+
evidencePolicy: DEFAULT_EVIDENCE_POLICY
|
|
2415
|
+
}, { jev: dependencies.jev });
|
|
2416
|
+
return { results: review.results, rules };
|
|
2417
|
+
}
|
|
2418
|
+
function ruleLaneUnavailable(turnId, reason) {
|
|
2419
|
+
return { results: [reviewLevelUnavailable(turnId, reason)], rules: [] };
|
|
2420
|
+
}
|
|
2421
|
+
function unavailableResolution(turnId, reason) {
|
|
2422
|
+
return { status: "UNAVAILABLE", lane: ruleLaneUnavailable(turnId, reason) };
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// packages/core/src/review/check-rules.ts
|
|
2426
|
+
async function checkRules(input, dependencies) {
|
|
2427
|
+
const { turn, ruleId } = input;
|
|
2428
|
+
const loaded = await loadPolicy(dependencies.policy);
|
|
2429
|
+
const resolution = resolveRuleCandidates(turn.id, loaded);
|
|
2430
|
+
if (resolution.status === "UNAVAILABLE") {
|
|
2431
|
+
return { status: "REVIEWED", review: aggregateReview(turn.id, resolution.lane.results) };
|
|
2432
|
+
}
|
|
2433
|
+
const candidates = selectCandidates(resolution.rules, ruleId);
|
|
2434
|
+
if (candidates.length === 0) {
|
|
2435
|
+
return { status: "RULE_NOT_FOUND" };
|
|
2436
|
+
}
|
|
2437
|
+
const config = readPolicyConfig(loaded, dependencies.parseConfig);
|
|
2438
|
+
const lane = await evaluateRuleCandidates(turn, candidates, config.gate, dependencies);
|
|
2439
|
+
return { status: "REVIEWED", review: aggregateReview(turn.id, lane.results) };
|
|
2440
|
+
}
|
|
2441
|
+
function selectCandidates(rules, ruleId) {
|
|
2442
|
+
if (ruleId === null) {
|
|
2443
|
+
return rules;
|
|
2444
|
+
}
|
|
2445
|
+
return rules.filter((candidate) => candidateId(candidate) === ruleId);
|
|
2446
|
+
}
|
|
2447
|
+
function candidateId(candidate) {
|
|
2448
|
+
return candidate.status === "PARSED" ? candidate.rule.id : candidate.ruleId;
|
|
2449
|
+
}
|
|
2450
|
+
// packages/core/src/review/review-turn.ts
|
|
2451
|
+
async function reviewTurn(turn, dependencies) {
|
|
2452
|
+
const loaded = await loadPolicy(dependencies.policy);
|
|
2453
|
+
const config = readPolicyConfig(loaded, dependencies.parseConfig);
|
|
2454
|
+
const builtInInput = { turn, evidencePolicy: DEFAULT_EVIDENCE_POLICY };
|
|
2455
|
+
const [ruleLane, builtIns] = await Promise.all([
|
|
2456
|
+
evaluateRuleLane(turn, loaded, config.gate, dependencies),
|
|
2457
|
+
evaluateBuiltIns(builtInInput, { jev: dependencies.jev })
|
|
2458
|
+
]);
|
|
2459
|
+
const review = aggregateReview(turn.id, [
|
|
2460
|
+
...ruleLane.results,
|
|
2461
|
+
builtIns.scopeCreep,
|
|
2462
|
+
builtIns.complexity
|
|
2463
|
+
]);
|
|
2464
|
+
return { review, rules: ruleLane.rules, remediation: config.remediation };
|
|
2465
|
+
}
|
|
2466
|
+
async function evaluateRuleLane(turn, loaded, gate, dependencies) {
|
|
2467
|
+
const resolution = resolveRuleCandidates(turn.id, loaded);
|
|
2468
|
+
if (resolution.status === "UNAVAILABLE") {
|
|
2469
|
+
return resolution.lane;
|
|
2470
|
+
}
|
|
2471
|
+
return evaluateRuleCandidates(turn, resolution.rules, gate, dependencies);
|
|
2472
|
+
}
|
|
1634
2473
|
// packages/plugin/src/skills/jevguard-rules/rules-summary.ts
|
|
1635
2474
|
function summarizeRules(text) {
|
|
1636
2475
|
const results = parseRules(text);
|