@iris-eval/mcp-server 0.9.0 → 0.10.0
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 +1 -1
- package/dist/config/defaults.js +15 -0
- package/dist/dashboard/assets/{index-Cz8_oOqG.js → index-CeJbaq6m.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/traces.js +2 -2
- package/dist/dashboard/seed-demo-data.js +1 -1
- package/dist/eval/citation-verify/verifier.d.ts +16 -1
- package/dist/eval/citation-verify/verifier.js +14 -4
- package/dist/eval/compose.d.ts +57 -0
- package/dist/eval/compose.js +179 -0
- package/dist/eval/criticality.d.ts +7 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/engine.d.ts +5 -2
- package/dist/eval/engine.js +81 -13
- package/dist/eval/llm-judge/evaluator.d.ts +20 -0
- package/dist/eval/llm-judge/evaluator.js +10 -1
- package/dist/eval/published-accuracy.d.ts +22 -22
- package/dist/eval/published-accuracy.js +11 -11
- package/dist/eval/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +5 -1
- package/dist/eval/rules/cost.d.ts +1 -1
- package/dist/eval/rules/cost.js +6 -6
- package/dist/eval/rules/custom.js +1 -0
- package/dist/eval/rules/relevance.js +7 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +55 -59
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +1 -1
- package/dist/eval/stamp.js +1 -0
- package/dist/eval/text/checksums.d.ts +23 -0
- package/dist/eval/text/checksums.js +97 -0
- package/dist/eval/text/normalise.d.ts +30 -0
- package/dist/eval/text/normalise.js +265 -0
- package/dist/eval/text/sentences.d.ts +15 -0
- package/dist/eval/text/sentences.js +149 -0
- package/dist/self-test.js +3 -3
- package/dist/storage/sqlite-adapter.js +16 -2
- package/dist/tools/evaluate-output.js +2 -2
- package/dist/tools/evaluate-with-llm-judge.d.ts +3 -0
- package/dist/tools/evaluate-with-llm-judge.js +29 -1
- package/dist/tools/verify-citations.d.ts +2 -1
- package/dist/tools/verify-citations.js +25 -4
- package/dist/types/config.d.ts +35 -0
- package/dist/types/eval.d.ts +51 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const PUBLISHED_ACCURACY_CORPUS_VERSION = "
|
|
2
|
-
export declare const PUBLISHED_ACCURACY_RELEASE = "0.
|
|
1
|
+
export declare const PUBLISHED_ACCURACY_CORPUS_VERSION = "4951762e456d";
|
|
2
|
+
export declare const PUBLISHED_ACCURACY_RELEASE = "0.10.0";
|
|
3
3
|
export declare const PUBLISHED_ACCURACY_LABELLING: "same-model";
|
|
4
4
|
export declare const PUBLISHED_ACCURACY: {
|
|
5
5
|
readonly min_output_length: {
|
|
@@ -34,17 +34,17 @@ export declare const PUBLISHED_ACCURACY: {
|
|
|
34
34
|
};
|
|
35
35
|
readonly sentence_count: {
|
|
36
36
|
readonly n: 30;
|
|
37
|
-
readonly tp:
|
|
37
|
+
readonly tp: 16;
|
|
38
38
|
readonly fp: 0;
|
|
39
|
-
readonly fn:
|
|
40
|
-
readonly tn:
|
|
39
|
+
readonly fn: 0;
|
|
40
|
+
readonly tn: 14;
|
|
41
41
|
readonly precision: 1;
|
|
42
|
-
readonly recall:
|
|
43
|
-
readonly f1:
|
|
42
|
+
readonly recall: 1;
|
|
43
|
+
readonly f1: 1;
|
|
44
44
|
readonly ci95: {
|
|
45
|
-
readonly precision: readonly [0.
|
|
46
|
-
readonly recall: readonly [0.
|
|
47
|
-
readonly f1: readonly [
|
|
45
|
+
readonly precision: readonly [0.8064, 1];
|
|
46
|
+
readonly recall: readonly [0.8064, 1];
|
|
47
|
+
readonly f1: readonly [1, 1];
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
readonly expected_coverage: {
|
|
@@ -93,33 +93,33 @@ export declare const PUBLISHED_ACCURACY: {
|
|
|
93
93
|
};
|
|
94
94
|
};
|
|
95
95
|
readonly no_pii: {
|
|
96
|
-
readonly n:
|
|
96
|
+
readonly n: 93;
|
|
97
97
|
readonly tp: 34;
|
|
98
98
|
readonly fp: 5;
|
|
99
99
|
readonly fn: 11;
|
|
100
|
-
readonly tn:
|
|
100
|
+
readonly tn: 43;
|
|
101
101
|
readonly precision: 0.8718;
|
|
102
102
|
readonly recall: 0.7556;
|
|
103
103
|
readonly f1: 0.8095;
|
|
104
104
|
readonly ci95: {
|
|
105
105
|
readonly precision: readonly [0.7329, 0.944];
|
|
106
106
|
readonly recall: readonly [0.6133, 0.8576];
|
|
107
|
-
readonly f1: readonly [0.
|
|
107
|
+
readonly f1: readonly [0.7077, 0.8932];
|
|
108
108
|
};
|
|
109
109
|
};
|
|
110
110
|
readonly no_blocklist_words: {
|
|
111
111
|
readonly n: 32;
|
|
112
|
-
readonly tp:
|
|
112
|
+
readonly tp: 14;
|
|
113
113
|
readonly fp: 1;
|
|
114
|
-
readonly fn:
|
|
114
|
+
readonly fn: 1;
|
|
115
115
|
readonly tn: 16;
|
|
116
|
-
readonly precision: 0.
|
|
117
|
-
readonly recall: 0.
|
|
118
|
-
readonly f1: 0.
|
|
116
|
+
readonly precision: 0.9333;
|
|
117
|
+
readonly recall: 0.9333;
|
|
118
|
+
readonly f1: 0.9333;
|
|
119
119
|
readonly ci95: {
|
|
120
|
-
readonly precision: readonly [0.
|
|
121
|
-
readonly recall: readonly [0.
|
|
122
|
-
readonly f1: readonly [0.
|
|
120
|
+
readonly precision: readonly [0.7018, 0.9881];
|
|
121
|
+
readonly recall: readonly [0.7018, 0.9881];
|
|
122
|
+
readonly f1: readonly [0.8148, 1];
|
|
123
123
|
};
|
|
124
124
|
};
|
|
125
125
|
readonly no_injection_patterns: {
|
|
@@ -197,7 +197,7 @@ export declare const PUBLISHED_ACCURACY: {
|
|
|
197
197
|
readonly f1: readonly [1, 1];
|
|
198
198
|
};
|
|
199
199
|
};
|
|
200
|
-
readonly
|
|
200
|
+
readonly verbosity_ratio: {
|
|
201
201
|
readonly n: 25;
|
|
202
202
|
readonly tp: 9;
|
|
203
203
|
readonly fp: 0;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* runner produces. Read by src/eval/accuracy.ts; the numbers a verdict carries
|
|
5
5
|
* are the numbers on https://iris-eval.com/proof, for the release named below.
|
|
6
6
|
*/
|
|
7
|
-
export const PUBLISHED_ACCURACY_CORPUS_VERSION = '
|
|
8
|
-
export const PUBLISHED_ACCURACY_RELEASE = '0.
|
|
7
|
+
export const PUBLISHED_ACCURACY_CORPUS_VERSION = '4951762e456d';
|
|
8
|
+
export const PUBLISHED_ACCURACY_RELEASE = '0.10.0';
|
|
9
9
|
export const PUBLISHED_ACCURACY_LABELLING = 'same-model';
|
|
10
10
|
export const PUBLISHED_ACCURACY = {
|
|
11
11
|
min_output_length: {
|
|
@@ -19,9 +19,9 @@ export const PUBLISHED_ACCURACY = {
|
|
|
19
19
|
ci95: { precision: [0.7225, 1], recall: [0.552, 0.953], f1: [0.7368, 1] },
|
|
20
20
|
},
|
|
21
21
|
sentence_count: {
|
|
22
|
-
n: 30, tp:
|
|
23
|
-
precision: 1, recall:
|
|
24
|
-
ci95: { precision: [0.
|
|
22
|
+
n: 30, tp: 16, fp: 0, fn: 0, tn: 14,
|
|
23
|
+
precision: 1, recall: 1, f1: 1,
|
|
24
|
+
ci95: { precision: [0.8064, 1], recall: [0.8064, 1], f1: [1, 1] },
|
|
25
25
|
},
|
|
26
26
|
expected_coverage: {
|
|
27
27
|
n: 29, tp: 14, fp: 0, fn: 0, tn: 15,
|
|
@@ -39,14 +39,14 @@ export const PUBLISHED_ACCURACY = {
|
|
|
39
39
|
ci95: { precision: [0.7412, 1], recall: [0.6461, 0.9851], f1: [0.8333, 1] },
|
|
40
40
|
},
|
|
41
41
|
no_pii: {
|
|
42
|
-
n:
|
|
42
|
+
n: 93, tp: 34, fp: 5, fn: 11, tn: 43,
|
|
43
43
|
precision: 0.8718, recall: 0.7556, f1: 0.8095,
|
|
44
|
-
ci95: { precision: [0.7329, 0.944], recall: [0.6133, 0.8576], f1: [0.
|
|
44
|
+
ci95: { precision: [0.7329, 0.944], recall: [0.6133, 0.8576], f1: [0.7077, 0.8932] },
|
|
45
45
|
},
|
|
46
46
|
no_blocklist_words: {
|
|
47
|
-
n: 32, tp:
|
|
48
|
-
precision: 0.
|
|
49
|
-
ci95: { precision: [0.
|
|
47
|
+
n: 32, tp: 14, fp: 1, fn: 1, tn: 16,
|
|
48
|
+
precision: 0.9333, recall: 0.9333, f1: 0.9333,
|
|
49
|
+
ci95: { precision: [0.7018, 0.9881], recall: [0.7018, 0.9881], f1: [0.8148, 1] },
|
|
50
50
|
},
|
|
51
51
|
no_injection_patterns: {
|
|
52
52
|
n: 90, tp: 41, fp: 0, fn: 1, tn: 48,
|
|
@@ -73,7 +73,7 @@ export const PUBLISHED_ACCURACY = {
|
|
|
73
73
|
precision: 1, recall: 1, f1: 1,
|
|
74
74
|
ci95: { precision: [0.7225, 1], recall: [0.7225, 1], f1: [1, 1] },
|
|
75
75
|
},
|
|
76
|
-
|
|
76
|
+
verbosity_ratio: {
|
|
77
77
|
n: 25, tp: 9, fp: 0, fn: 0, tn: 16,
|
|
78
78
|
precision: 1, recall: 1, f1: 1,
|
|
79
79
|
ci95: { precision: [0.7009, 1], recall: [0.7009, 1], f1: [1, 1] },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { EvalResult, FailureClass } from '../types/eval.js';
|
|
2
|
+
export declare const RISK_DRAWS = 2000;
|
|
3
|
+
export declare const DEFAULT_PRIOR = 0.5;
|
|
4
|
+
/**
|
|
5
|
+
* What the prior means (arc 2 finding, for arc 3's deliberation):
|
|
6
|
+
* 'per-class' — plan §4.3 as written: π is the prior that EACH examined
|
|
7
|
+
* class is present. With K classes examined the prior that
|
|
8
|
+
* nothing is wrong is (1 − π)^K — at π = 0.5 and K = 10 it
|
|
9
|
+
* is 0.001, so the noisy-OR blocks nearly everything.
|
|
10
|
+
* 'per-output' — π is the prior that the OUTPUT is bad (any class
|
|
11
|
+
* present); it is spread over the K examined classes as
|
|
12
|
+
* π_c = 1 − (1 − π)^(1/K), so that Π(1 − π_c) = 1 − π.
|
|
13
|
+
* Both are measured on the composite corpus so the choice is made on a
|
|
14
|
+
* number, not a preference.
|
|
15
|
+
*/
|
|
16
|
+
export type PriorMode = 'per-class' | 'per-output';
|
|
17
|
+
export declare const DEFAULT_PRIOR_MODE: PriorMode;
|
|
18
|
+
/** τ = 1 / (1 + c) with c = 1: a false pass costs the same as a false block. */
|
|
19
|
+
export declare const DEFAULT_FALSE_PASS_COST = 1;
|
|
20
|
+
export declare const DEFAULT_TAU: number;
|
|
21
|
+
export interface RiskEstimate {
|
|
22
|
+
pBad: number;
|
|
23
|
+
lo: number;
|
|
24
|
+
hi: number;
|
|
25
|
+
perClass: Record<string, number | null>;
|
|
26
|
+
assumptions: string[];
|
|
27
|
+
}
|
|
28
|
+
export type RiskBasis = 'policy_gate' | 'detector_veto' | 'risk_over_loss' | 'clean' | 'no_rules';
|
|
29
|
+
export interface RiskVerdict {
|
|
30
|
+
state: 'pass' | 'fail' | 'unknown';
|
|
31
|
+
basis: RiskBasis;
|
|
32
|
+
by: string[];
|
|
33
|
+
risk: RiskEstimate | null;
|
|
34
|
+
confidence: 'decisive' | 'marginal' | null;
|
|
35
|
+
}
|
|
36
|
+
interface Detector {
|
|
37
|
+
name: string;
|
|
38
|
+
classes: FailureClass[];
|
|
39
|
+
fired: boolean;
|
|
40
|
+
counts: {
|
|
41
|
+
tp: number;
|
|
42
|
+
fp: number;
|
|
43
|
+
fn: number;
|
|
44
|
+
tn: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The evaluated detections and inferences with a published family, one entry per rule. */
|
|
48
|
+
export declare function detectorsOf(result: EvalResult): Detector[];
|
|
49
|
+
/** The per-class prior under a mode, given how many classes the detectors examine. */
|
|
50
|
+
export declare function classPrior(prior: number, mode: PriorMode, examinedClasses: number): number;
|
|
51
|
+
/** p_bad with a 95% credible interval from the Beta posteriors of every detector's sensitivity and specificity. */
|
|
52
|
+
export declare function riskEstimate(result: EvalResult, prior?: number, mode?: PriorMode): RiskEstimate | null;
|
|
53
|
+
/**
|
|
54
|
+
* Compose by kind, as arc 3 will: gates (a failing policy that is effectively
|
|
55
|
+
* critical here), then vetoes (a failing effectively-critical detection),
|
|
56
|
+
* then the risk against τ. `unknown` when a critical rule was asked and could
|
|
57
|
+
* not answer (defeated or config_invalid) — the fail-closed seam.
|
|
58
|
+
*/
|
|
59
|
+
export declare function riskVerdict(result: EvalResult, tau?: number, prior?: number, mode?: PriorMode): RiskVerdict;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { publishedAccuracyFor } from './accuracy.js';
|
|
2
|
+
import { PUBLISHED_ACCURACY_CORPUS_VERSION } from './published-accuracy.js';
|
|
3
|
+
import { FAILURE_CLASS_IDS } from './failure-classes.js';
|
|
4
|
+
import { fnv1a, mulberry32 } from './seeded-random.js';
|
|
5
|
+
/** Jeffreys prior: half a count on each cell, so a family that made no mistakes does not claim certainty. */
|
|
6
|
+
const HALF = 0.5;
|
|
7
|
+
const sensOf = (d) => (d.counts.tp + HALF) / (d.counts.tp + d.counts.fn + 2 * HALF);
|
|
8
|
+
const specOf = (d) => (d.counts.tn + HALF) / (d.counts.tn + d.counts.fp + 2 * HALF);
|
|
9
|
+
export const RISK_DRAWS = 2000;
|
|
10
|
+
export const DEFAULT_PRIOR = 0.5;
|
|
11
|
+
export const DEFAULT_PRIOR_MODE = 'per-output';
|
|
12
|
+
/** τ = 1 / (1 + c) with c = 1: a false pass costs the same as a false block. */
|
|
13
|
+
export const DEFAULT_FALSE_PASS_COST = 1;
|
|
14
|
+
export const DEFAULT_TAU = 1 / (1 + DEFAULT_FALSE_PASS_COST);
|
|
15
|
+
function beta(a, b, rng) {
|
|
16
|
+
// Marsaglia–Tsang gamma with the shape < 1 boost; beta = ga / (ga + gb).
|
|
17
|
+
const gamma = (shape) => {
|
|
18
|
+
if (shape < 1)
|
|
19
|
+
return gamma(shape + 1) * Math.pow(rng(), 1 / shape);
|
|
20
|
+
const d = shape - 1 / 3;
|
|
21
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
22
|
+
for (;;) {
|
|
23
|
+
let x;
|
|
24
|
+
let v;
|
|
25
|
+
do {
|
|
26
|
+
const u1 = rng();
|
|
27
|
+
const u2 = rng();
|
|
28
|
+
x = Math.sqrt(-2 * Math.log(u1 || 1e-12)) * Math.cos(2 * Math.PI * u2);
|
|
29
|
+
v = 1 + c * x;
|
|
30
|
+
} while (v <= 0);
|
|
31
|
+
v = v * v * v;
|
|
32
|
+
const u = rng();
|
|
33
|
+
if (u < 1 - 0.0331 * x * x * x * x)
|
|
34
|
+
return d * v;
|
|
35
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
|
|
36
|
+
return d * v;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const ga = gamma(a);
|
|
40
|
+
const gb = gamma(b);
|
|
41
|
+
return ga / (ga + gb);
|
|
42
|
+
}
|
|
43
|
+
/** The evaluated detections and inferences with a published family, one entry per rule. */
|
|
44
|
+
export function detectorsOf(result) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const r of result.rule_results) {
|
|
47
|
+
if (r.skipped)
|
|
48
|
+
continue;
|
|
49
|
+
if (r.kind !== 'detection' && r.kind !== 'inference')
|
|
50
|
+
continue;
|
|
51
|
+
const acc = publishedAccuracyFor(r.ruleName);
|
|
52
|
+
if (!acc)
|
|
53
|
+
continue;
|
|
54
|
+
out.push({
|
|
55
|
+
name: r.ruleName,
|
|
56
|
+
classes: (r.classes ?? []),
|
|
57
|
+
fired: r.passed === false,
|
|
58
|
+
counts: { tp: acc.tp, fp: acc.fp, fn: acc.fn, tn: acc.tn },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** The per-class prior under a mode, given how many classes the detectors examine. */
|
|
64
|
+
export function classPrior(prior, mode, examinedClasses) {
|
|
65
|
+
if (mode === 'per-class' || examinedClasses <= 1)
|
|
66
|
+
return prior;
|
|
67
|
+
return 1 - Math.pow(1 - prior, 1 / examinedClasses);
|
|
68
|
+
}
|
|
69
|
+
function pBadFrom(detectors, prior, mode, sensOf, specOf) {
|
|
70
|
+
const perClass = {};
|
|
71
|
+
let survive = 1;
|
|
72
|
+
const examinedClasses = FAILURE_CLASS_IDS.filter((cls) => detectors.some((d) => d.classes.includes(cls))).length;
|
|
73
|
+
const priorC = classPrior(prior, mode, examinedClasses);
|
|
74
|
+
for (const cls of FAILURE_CLASS_IDS) {
|
|
75
|
+
const examined = detectors.filter((d) => d.classes.includes(cls));
|
|
76
|
+
if (examined.length === 0) {
|
|
77
|
+
perClass[cls] = null;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const fired = examined.filter((d) => d.fired);
|
|
81
|
+
let q;
|
|
82
|
+
if (fired.length > 0) {
|
|
83
|
+
q = Math.max(...fired.map((d) => {
|
|
84
|
+
const s = sensOf(d);
|
|
85
|
+
const p = specOf(d);
|
|
86
|
+
const den = s * priorC + (1 - p) * (1 - priorC);
|
|
87
|
+
return den === 0 ? 0 : (s * priorC) / den;
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
let missAll = 1;
|
|
92
|
+
let specAll = 1;
|
|
93
|
+
for (const d of examined) {
|
|
94
|
+
missAll *= 1 - sensOf(d);
|
|
95
|
+
specAll *= specOf(d);
|
|
96
|
+
}
|
|
97
|
+
const den = priorC * missAll + (1 - priorC) * specAll;
|
|
98
|
+
q = den === 0 ? 0 : (priorC * missAll) / den;
|
|
99
|
+
}
|
|
100
|
+
perClass[cls] = q;
|
|
101
|
+
survive *= 1 - q;
|
|
102
|
+
}
|
|
103
|
+
return { pBad: 1 - survive, perClass };
|
|
104
|
+
}
|
|
105
|
+
const round4 = (x) => Math.round(x * 10_000) / 10_000;
|
|
106
|
+
/** p_bad with a 95% credible interval from the Beta posteriors of every detector's sensitivity and specificity. */
|
|
107
|
+
export function riskEstimate(result, prior = DEFAULT_PRIOR, mode = DEFAULT_PRIOR_MODE) {
|
|
108
|
+
const detectors = detectorsOf(result);
|
|
109
|
+
if (detectors.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
/*
|
|
112
|
+
* Jeffreys half-counts in the POINT estimate, not only in the draws.
|
|
113
|
+
*
|
|
114
|
+
* Twelve of the fifteen published families recorded zero false positives.
|
|
115
|
+
* With specificity exactly 1 the positive predictive value of a fire is
|
|
116
|
+
* exactly 1 at every prior, so p_bad read exactly 1.000 on 31 of the 111
|
|
117
|
+
* composite cases — the same overconfidence the credible interval was
|
|
118
|
+
* added to cure, reintroduced one layer down. A half-count on each cell
|
|
119
|
+
* says what thirty cases can actually support: no_silent_tool_failure's
|
|
120
|
+
* single-fire contribution moves from 1.000 to about 0.69, still well
|
|
121
|
+
* over the shipped threshold, and now with an interval that means
|
|
122
|
+
* something. Found by arc 2 while writing up the composer, not by
|
|
123
|
+
* reading it.
|
|
124
|
+
*/
|
|
125
|
+
const point = pBadFrom(detectors, prior, mode, sensOf, specOf);
|
|
126
|
+
const rng = mulberry32(fnv1a(`risk:${PUBLISHED_ACCURACY_CORPUS_VERSION}:${mode}:${prior.toFixed(3)}:${detectors.map((d) => `${d.name}${d.fired ? '!' : ''}`).join(',')}`));
|
|
127
|
+
const draws = [];
|
|
128
|
+
for (let i = 0; i < RISK_DRAWS; i++) {
|
|
129
|
+
const sens = new Map();
|
|
130
|
+
const spec = new Map();
|
|
131
|
+
for (const d of detectors) {
|
|
132
|
+
sens.set(d.name, beta(d.counts.tp + 0.5, d.counts.fn + 0.5, rng));
|
|
133
|
+
spec.set(d.name, beta(d.counts.tn + 0.5, d.counts.fp + 0.5, rng));
|
|
134
|
+
}
|
|
135
|
+
draws.push(pBadFrom(detectors, prior, mode, (d) => sens.get(d.name), (d) => spec.get(d.name)).pBad);
|
|
136
|
+
}
|
|
137
|
+
draws.sort((a, b) => a - b);
|
|
138
|
+
const at = (q) => draws[Math.min(draws.length - 1, Math.max(0, Math.ceil(q * draws.length) - 1))];
|
|
139
|
+
// The point uses the observed rates; a rate at exactly 1 (no false positives
|
|
140
|
+
// in the family) puts the point above every posterior draw, so the interval
|
|
141
|
+
// is widened to contain it — an interval that excludes its own point is a
|
|
142
|
+
// presentation error, not a finding.
|
|
143
|
+
return {
|
|
144
|
+
pBad: round4(point.pBad),
|
|
145
|
+
lo: round4(Math.min(at(0.025), point.pBad)),
|
|
146
|
+
hi: round4(Math.max(at(0.975), point.pBad)),
|
|
147
|
+
perClass: Object.fromEntries(Object.entries(point.perClass).map(([k, v]) => [k, v === null ? null : round4(v)])),
|
|
148
|
+
assumptions: [
|
|
149
|
+
'detectors independent across classes',
|
|
150
|
+
'published accuracy is in-sample, same-model labelled',
|
|
151
|
+
'sensitivity and specificity carry a half-count prior, so a family with no observed errors does not read as certain',
|
|
152
|
+
`prior ${prior}, spread ${mode}`,
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const isEffectivelyCritical = (r) => r.critical === true;
|
|
157
|
+
/**
|
|
158
|
+
* Compose by kind, as arc 3 will: gates (a failing policy that is effectively
|
|
159
|
+
* critical here), then vetoes (a failing effectively-critical detection),
|
|
160
|
+
* then the risk against τ. `unknown` when a critical rule was asked and could
|
|
161
|
+
* not answer (defeated or config_invalid) — the fail-closed seam.
|
|
162
|
+
*/
|
|
163
|
+
export function riskVerdict(result, tau = DEFAULT_TAU, prior = DEFAULT_PRIOR, mode = DEFAULT_PRIOR_MODE) {
|
|
164
|
+
const rows = result.rule_results;
|
|
165
|
+
const gates = rows.filter((r) => r.kind === 'policy' && !r.skipped && r.passed === false && isEffectivelyCritical(r));
|
|
166
|
+
if (gates.length > 0)
|
|
167
|
+
return { state: 'fail', basis: 'policy_gate', by: gates.map((r) => r.ruleName), risk: riskEstimate(result, prior, mode), confidence: null };
|
|
168
|
+
const vetoes = rows.filter((r) => (r.kind === 'detection' || r.kind === 'inference') && !r.skipped && r.passed === false && isEffectivelyCritical(r));
|
|
169
|
+
if (vetoes.length > 0)
|
|
170
|
+
return { state: 'fail', basis: 'detector_veto', by: vetoes.map((r) => r.ruleName), risk: riskEstimate(result, prior, mode), confidence: null };
|
|
171
|
+
const unknown = rows.filter((r) => isEffectivelyCritical(r) && r.skipped && r.skipClass && r.skipClass !== 'not_applicable');
|
|
172
|
+
if (unknown.length > 0)
|
|
173
|
+
return { state: 'unknown', basis: 'clean', by: unknown.map((r) => r.ruleName), risk: null, confidence: null };
|
|
174
|
+
const risk = riskEstimate(result, prior, mode);
|
|
175
|
+
if (!risk) {
|
|
176
|
+
const judged = rows.some((r) => !r.skipped);
|
|
177
|
+
return { state: judged ? 'pass' : 'unknown', basis: judged ? 'clean' : 'no_rules', by: [], risk: null, confidence: null };
|
|
178
|
+
}
|
|
179
|
+
const confidence = risk.lo <= tau && tau <= risk.hi ? 'marginal' : 'decisive';
|
|
180
|
+
if (risk.pBad > tau) {
|
|
181
|
+
const by = Object.entries(risk.perClass)
|
|
182
|
+
.filter(([, q]) => q !== null && q > 0.5)
|
|
183
|
+
.map(([cls]) => cls);
|
|
184
|
+
return { state: 'fail', basis: 'risk_over_loss', by, risk, confidence };
|
|
185
|
+
}
|
|
186
|
+
return { state: 'pass', basis: 'clean', by: [], risk, confidence };
|
|
187
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countSentences } from '../text/sentences.js';
|
|
1
2
|
export const minOutputLength = {
|
|
2
3
|
name: 'min_output_length',
|
|
3
4
|
description: 'Output must meet a minimum character length',
|
|
@@ -60,7 +61,10 @@ export const sentenceCount = {
|
|
|
60
61
|
version: 1,
|
|
61
62
|
evaluate(context) {
|
|
62
63
|
const minSentences = context.customConfig?.min_sentences ?? 2;
|
|
63
|
-
|
|
64
|
+
// One splitter, shared with topic_consistency (src/eval/text/sentences.ts).
|
|
65
|
+
// Splitting on /[.!?]+/ counted "The latency is 3.5 seconds." as two
|
|
66
|
+
// sentences and "Dr. Chen approved it." as two more.
|
|
67
|
+
const sentences = countSentences(context.output);
|
|
64
68
|
const passed = sentences >= minSentences;
|
|
65
69
|
return {
|
|
66
70
|
ruleName: 'sentence_count',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type EvalRule } from '../../types/eval.js';
|
|
2
2
|
export declare const costUnderThreshold: EvalRule;
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const verbosityRatio: EvalRule;
|
|
4
4
|
/** Default for config key `max_tool_repeats`: how many identical calls are tolerated. */
|
|
5
5
|
export declare const DEFAULT_MAX_TOOL_REPEATS = 3;
|
|
6
6
|
/**
|
package/dist/eval/rules/cost.js
CHANGED
|
@@ -30,9 +30,9 @@ export const costUnderThreshold = {
|
|
|
30
30
|
};
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
|
-
export const
|
|
34
|
-
name: '
|
|
35
|
-
description: '
|
|
33
|
+
export const verbosityRatio = {
|
|
34
|
+
name: 'verbosity_ratio',
|
|
35
|
+
description: 'The completion-to-prompt token ratio against a ceiling: completion_tokens / prompt_tokens must not exceed max_token_ratio (default 5). This measures output VERBOSITY relative to prompt size, not efficiency — a long answer to a long prompt passes and a long answer to a short prompt fails, and neither says whether the tokens were well spent. Skipped when token usage is not supplied. Renamed from token_efficiency in 0.10.0, because the old name named something the rule does not measure',
|
|
36
36
|
evalType: 'cost',
|
|
37
37
|
weight: 0.5,
|
|
38
38
|
kind: 'measurement',
|
|
@@ -45,13 +45,13 @@ export const tokenEfficiency = {
|
|
|
45
45
|
const prompt = context.tokenUsage?.prompt_tokens;
|
|
46
46
|
const completion = context.tokenUsage?.completion_tokens;
|
|
47
47
|
if (prompt === undefined || completion === undefined || prompt === 0) {
|
|
48
|
-
return { ruleName: '
|
|
48
|
+
return { ruleName: 'verbosity_ratio', passed: false, score: 0, message: 'Token usage not provided', skipped: true, skipReason: 'context.tokenUsage not provided' };
|
|
49
49
|
}
|
|
50
50
|
const ratio = completion / prompt;
|
|
51
51
|
const maxRatio = context.customConfig?.max_token_ratio ?? 5;
|
|
52
52
|
const passed = ratio <= maxRatio;
|
|
53
53
|
return {
|
|
54
|
-
ruleName: '
|
|
54
|
+
ruleName: 'verbosity_ratio',
|
|
55
55
|
passed,
|
|
56
56
|
score: passed ? 1 : Math.max(0, 1 - (ratio - maxRatio) / maxRatio),
|
|
57
57
|
value: { stat: 'completion_to_prompt_ratio', unit: 'ratio', value: ratio },
|
|
@@ -184,4 +184,4 @@ export const noToolLoop = {
|
|
|
184
184
|
};
|
|
185
185
|
},
|
|
186
186
|
};
|
|
187
|
-
export const costRules = [costUnderThreshold,
|
|
187
|
+
export const costRules = [costUnderThreshold, verbosityRatio, noToolLoop];
|
|
@@ -244,6 +244,7 @@ export function createCustomRule(definition, severity) {
|
|
|
244
244
|
weight: definition.weight ?? 1,
|
|
245
245
|
critical: severity === 'high' || severity === 'critical',
|
|
246
246
|
kind: 'policy',
|
|
247
|
+
origin: 'custom',
|
|
247
248
|
mechanism: meta?.mechanism ?? 'formula',
|
|
248
249
|
needs: meta?.needs ?? ['output'],
|
|
249
250
|
classes: [],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sentencesOf } from '../text/sentences.js';
|
|
1
2
|
/*
|
|
2
3
|
* Relevance rules — one tokenizer, two DISTINCT signals.
|
|
3
4
|
*
|
|
@@ -179,7 +180,11 @@ export const keywordOverlap = {
|
|
|
179
180
|
*/
|
|
180
181
|
const DEFAULT_TOPIC_THRESHOLD = 1 / 3;
|
|
181
182
|
const LIST_ITEM = /^\s*(?:[-*+•]|\d{1,3}[.)])\s+/;
|
|
182
|
-
|
|
183
|
+
/*
|
|
184
|
+
* Replaced by the shared splitter (src/eval/text/sentences.ts). The old
|
|
185
|
+
* pattern broke after any terminator followed by whitespace, so "Dr. Chen"
|
|
186
|
+
* and "3. 5" were two sentences each.
|
|
187
|
+
*/
|
|
183
188
|
export const topicConsistency = {
|
|
184
189
|
name: 'topic_consistency',
|
|
185
190
|
description: 'Continuity with the input: the share of the output\'s content-bearing sentences that connect to the input\'s topic — a sentence connects when it shares a content term with the input or with an earlier connected sentence (list items are read under the sentence that introduces them). Passes when at least a third connect (configurable: topic_consistency); a third, not half, so a short honest answer that elaborates in fresh words is not read as drift. Replaces the output-word-ratio measure that failed every grounded technical answer. Skipped when the output is too brief for meaningful comparison',
|
|
@@ -227,7 +232,7 @@ export const topicConsistency = {
|
|
|
227
232
|
for (const line of context.output.replace(FENCED_CODE, '\n').split('\n')) {
|
|
228
233
|
const isItem = LIST_ITEM.test(line);
|
|
229
234
|
let lineConnected = false;
|
|
230
|
-
for (const sentence of line
|
|
235
|
+
for (const sentence of sentencesOf(line)) {
|
|
231
236
|
const terms = contentTerms(sentence);
|
|
232
237
|
if (terms.length === 0)
|
|
233
238
|
continue;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { EvalRule } from '../../types/eval.js';
|
|
2
|
-
export
|
|
2
|
+
export interface PiiPattern {
|
|
3
3
|
name: string;
|
|
4
4
|
pattern: RegExp;
|
|
5
|
+
/** Documentation values this pattern should recognise and ignore. */
|
|
5
6
|
placeholders?: RegExp[];
|
|
6
|
-
|
|
7
|
+
/** The structural check described above; a match that fails it is not a match. */
|
|
8
|
+
validate?: (match: string) => boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const PII_PATTERNS: PiiPattern[];
|
|
7
11
|
/**
|
|
8
12
|
* The pass message when placeholders were ignored. Says so explicitly,
|
|
9
13
|
* with the count and the pattern names (#370): a builder smoke-testing with
|