@iris-eval/mcp-server 0.8.2 → 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 +9 -2
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +17 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/capabilities.d.ts +3 -0
- package/dist/dashboard/routes/capabilities.js +11 -0
- package/dist/dashboard/routes/health.d.ts +5 -1
- package/dist/dashboard/routes/health.js +15 -3
- package/dist/dashboard/routes/rules.js +4 -1
- package/dist/dashboard/routes/traces.d.ts +3 -0
- package/dist/dashboard/routes/traces.js +11 -30
- package/dist/dashboard/seed-demo-data.js +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +6 -2
- package/dist/eval/accuracy.d.ts +41 -0
- package/dist/eval/accuracy.js +97 -0
- 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 +15 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +6 -2
- package/dist/eval/engine.js +126 -12
- package/dist/eval/failure-classes.d.ts +8 -0
- package/dist/eval/failure-classes.js +18 -0
- package/dist/eval/llm-judge/evaluator.d.ts +30 -0
- package/dist/eval/llm-judge/evaluator.js +26 -2
- package/dist/eval/published-accuracy.d.ts +230 -0
- package/dist/eval/published-accuracy.js +86 -0
- package/dist/eval/questions.d.ts +12 -0
- package/dist/eval/questions.js +14 -0
- package/dist/eval/response-schema.d.ts +652 -0
- package/dist/eval/response-schema.js +130 -0
- package/dist/eval/response.d.ts +12 -0
- package/dist/eval/response.js +30 -0
- package/dist/eval/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +36 -1
- package/dist/eval/rules/cost.d.ts +2 -2
- package/dist/eval/rules/cost.js +50 -6
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +22 -0
- package/dist/eval/rules/relevance.js +23 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +224 -51
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +89 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -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/eval/verdict.d.ts +34 -0
- package/dist/eval/verdict.js +131 -0
- package/dist/index.js +5 -28
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +53 -0
- package/dist/judge-enablement.d.ts +34 -0
- package/dist/judge-enablement.js +78 -0
- package/dist/judge-enablement.json +10 -0
- package/dist/preferences.d.ts +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +29 -0
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.js +65 -5
- package/dist/resources/uris.d.ts +12 -0
- package/dist/resources/uris.js +24 -0
- package/dist/retention.d.ts +20 -0
- package/dist/retention.js +44 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +17 -3
- package/dist/server.d.ts +10 -1
- package/dist/server.js +34 -7
- package/dist/storage/index.js +1 -1
- package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
- package/dist/storage/migrations/007-eval-provenance.js +30 -0
- package/dist/storage/migrations/index.js +24 -4
- package/dist/storage/sqlite-adapter.d.ts +26 -1
- package/dist/storage/sqlite-adapter.js +149 -15
- package/dist/tools/delete-rule.d.ts +8 -0
- package/dist/tools/delete-rule.js +30 -38
- package/dist/tools/delete-trace.d.ts +5 -0
- package/dist/tools/delete-trace.js +24 -27
- package/dist/tools/deploy-rule.d.ts +13 -1
- package/dist/tools/deploy-rule.js +37 -34
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +36 -0
- package/dist/tools/errors.d.ts +36 -0
- package/dist/tools/errors.js +134 -0
- package/dist/tools/evaluate-output.d.ts +8 -1
- package/dist/tools/evaluate-output.js +39 -60
- package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
- package/dist/tools/evaluate-with-llm-judge.js +124 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +29 -28
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +22 -1
- package/dist/tools/list-rules.d.ts +13 -0
- package/dist/tools/list-rules.js +43 -46
- package/dist/tools/log-trace.d.ts +4 -0
- package/dist/tools/log-trace.js +31 -29
- package/dist/tools/respond.d.ts +42 -0
- package/dist/tools/respond.js +90 -0
- package/dist/tools/strict-input.js +1 -1
- package/dist/tools/trace-link.d.ts +2 -0
- package/dist/tools/trace-link.js +13 -2
- package/dist/tools/verify-citations.d.ts +18 -2
- package/dist/tools/verify-citations.js +122 -96
- package/dist/types/config.d.ts +44 -0
- package/dist/types/eval.d.ts +309 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/resources/dashboard-summary.d.ts +0 -3
- package/dist/resources/dashboard-summary.js +0 -16
- package/dist/resources/trace-detail.d.ts +0 -3
- package/dist/resources/trace-detail.js +0 -30
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EvalRule, EvalType } from '../types/eval.js';
|
|
1
|
+
import type { EvalRule, EvalType, ClaimKind, Mechanism, Need, QuestionId, FailureClass } from '../types/eval.js';
|
|
2
2
|
/** Where a rule's EFFECTIVE criticality came from. */
|
|
3
3
|
export type CriticalitySource = 'default' | 'config';
|
|
4
4
|
export interface CriticalityOverrides {
|
|
@@ -6,6 +6,13 @@ export interface CriticalityOverrides {
|
|
|
6
6
|
criticalRules?: string[];
|
|
7
7
|
/** Built-in rule names demoted from critical. */
|
|
8
8
|
nonCriticalRules?: string[];
|
|
9
|
+
composer?: 'risk' | 'legacy';
|
|
10
|
+
falsePassCost?: number;
|
|
11
|
+
onCriticalSkipped?: 'unknown' | 'fail' | 'pass';
|
|
12
|
+
requiredEvidence?: string[];
|
|
13
|
+
defaultsGate?: boolean;
|
|
14
|
+
prior?: number;
|
|
15
|
+
priorMode?: 'per-output' | 'per-class';
|
|
9
16
|
}
|
|
10
17
|
export interface EffectiveCriticality {
|
|
11
18
|
critical: boolean;
|
|
@@ -50,6 +57,13 @@ export interface BuiltInRuleMeta {
|
|
|
50
57
|
critical: boolean;
|
|
51
58
|
/** Who decided it: the rule's own declaration, or one of the config lists. */
|
|
52
59
|
criticalSource: CriticalitySource;
|
|
60
|
+
/** The rule's declared metadata (see EvalRule): the kind of claim, its mechanism, what it reads, the question it answers, the failure classes, the definition version. */
|
|
61
|
+
kind?: ClaimKind;
|
|
62
|
+
mechanism?: Mechanism;
|
|
63
|
+
needs?: readonly Need[];
|
|
64
|
+
question?: QuestionId;
|
|
65
|
+
classes?: readonly FailureClass[];
|
|
66
|
+
version?: number;
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
55
69
|
* The whole built-in roster, one entry per rule.
|
package/dist/eval/criticality.js
CHANGED
|
@@ -147,6 +147,12 @@ export function builtInRuleRoster(resolve) {
|
|
|
147
147
|
weight: rule.weight,
|
|
148
148
|
critical: effective.critical,
|
|
149
149
|
criticalSource: effective.source,
|
|
150
|
+
kind: rule.kind,
|
|
151
|
+
mechanism: rule.mechanism,
|
|
152
|
+
needs: rule.needs,
|
|
153
|
+
question: rule.question,
|
|
154
|
+
classes: rule.classes,
|
|
155
|
+
version: rule.version,
|
|
150
156
|
});
|
|
151
157
|
}
|
|
152
158
|
}
|
|
@@ -104,14 +104,34 @@ function computeRuleSnapshot(evals) {
|
|
|
104
104
|
}
|
|
105
105
|
return { failed, skipped, passedCount, totalCount };
|
|
106
106
|
}
|
|
107
|
+
/*
|
|
108
|
+
* The moment SHOWS the verdict each evaluation reached; it does not compute
|
|
109
|
+
* a second one.
|
|
110
|
+
*
|
|
111
|
+
* It used to count failed rules: no failures meant pass, no passes meant
|
|
112
|
+
* fail, anything else meant partial. From 0.10.0 those two answers diverge,
|
|
113
|
+
* and the divergence is the whole point of the composer. An evaluation can
|
|
114
|
+
* pass with a rule visibly failed — a shipped default that only advises, or
|
|
115
|
+
* evidence too weak to carry the risk past the deployment's loss threshold
|
|
116
|
+
* — and the old arithmetic would have called that "partial", contradicting
|
|
117
|
+
* the verdict the tool returned for the same evaluation.
|
|
118
|
+
*
|
|
119
|
+
* "partial" now means what it says: several evaluations of one trace and
|
|
120
|
+
* they did not agree. An `unknown` verdict reads as unevaluated, because
|
|
121
|
+
* that is what it is — asked, and unable to answer.
|
|
122
|
+
*/
|
|
107
123
|
function computeVerdict(evals, snapshot) {
|
|
108
124
|
if (evals.length === 0)
|
|
109
125
|
return 'unevaluated';
|
|
110
126
|
if (snapshot.totalCount - snapshot.skipped.length === 0)
|
|
111
127
|
return 'unevaluated';
|
|
112
|
-
|
|
128
|
+
const decided = evals.filter((e) => e.verdict === undefined || e.verdict.state !== 'unknown');
|
|
129
|
+
if (decided.length === 0)
|
|
130
|
+
return 'unevaluated';
|
|
131
|
+
const passed = decided.filter((e) => e.passed).length;
|
|
132
|
+
if (passed === decided.length)
|
|
113
133
|
return 'pass';
|
|
114
|
-
if (
|
|
134
|
+
if (passed === 0)
|
|
115
135
|
return 'fail';
|
|
116
136
|
return 'partial';
|
|
117
137
|
}
|
|
@@ -122,8 +142,17 @@ function computeOverallScore(evals) {
|
|
|
122
142
|
return sum / evals.length;
|
|
123
143
|
}
|
|
124
144
|
function classifySignificance({ trace, evals, ruleSnapshot, verdict, }) {
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
/*
|
|
146
|
+
* 1. Safety violation — a rule that VETOES failed, or a safety-bundle rule
|
|
147
|
+
* did. Bundle membership alone was the old test, and it is the weaker
|
|
148
|
+
* one: from 0.10.0 which rules veto is the deployment's call
|
|
149
|
+
* (eval.criticalRules), so a rule promoted to critical outside the safety
|
|
150
|
+
* bundle is exactly as serious and used to rank as a plain failure. The
|
|
151
|
+
* bundle list stays as well, because a safety rule that a deployment
|
|
152
|
+
* DEMOTED still describes what it found.
|
|
153
|
+
*/
|
|
154
|
+
const vetoed = new Set(evals.flatMap((e) => e.rule_results.filter((r) => !r.skipped && r.passed === false && r.role === 'veto').map((r) => r.ruleName)));
|
|
155
|
+
const safetyFailed = ruleSnapshot.failed.filter((name) => SAFETY_RULE_NAMES.has(name) || vetoed.has(name));
|
|
127
156
|
if (safetyFailed.length > 0) {
|
|
128
157
|
return {
|
|
129
158
|
kind: 'safety-violation',
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Coverage } from '../types/eval.js';
|
|
2
|
+
export type DormantRule = NonNullable<Coverage['dormant']>[number];
|
|
3
|
+
/** The quarantined entries that would have gated, as `coverage.dormant` rows. */
|
|
4
|
+
export declare function dormantRulesFrom(quarantined: readonly unknown[]): DormantRule[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const GATING = new Set(['high', 'critical']);
|
|
2
|
+
function field(entry, key) {
|
|
3
|
+
if (!entry || typeof entry !== 'object')
|
|
4
|
+
return undefined;
|
|
5
|
+
const value = entry[key];
|
|
6
|
+
return typeof value === 'string' ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
/** The quarantined entries that would have gated, as `coverage.dormant` rows. */
|
|
9
|
+
export function dormantRulesFrom(quarantined) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const entry of quarantined) {
|
|
12
|
+
const severity = field(entry, 'severity');
|
|
13
|
+
if (!severity || !GATING.has(severity))
|
|
14
|
+
continue;
|
|
15
|
+
out.push({
|
|
16
|
+
ruleId: field(entry, 'id') ?? 'unknown',
|
|
17
|
+
name: field(entry, 'name') ?? 'unnamed',
|
|
18
|
+
reason: `quarantined: the stored definition failed validation in this version, so this ${severity} rule is not running`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
package/dist/eval/engine.d.ts
CHANGED
|
@@ -39,12 +39,15 @@ export declare class EvalEngine {
|
|
|
39
39
|
private idByRule;
|
|
40
40
|
private threshold;
|
|
41
41
|
private ruleThresholds?;
|
|
42
|
+
private criticalityOverrides?;
|
|
42
43
|
/**
|
|
43
44
|
* Effective criticality per rule, bound to this engine's config overrides.
|
|
44
45
|
* Every veto decision reads THIS, never `rule.critical` directly, so a
|
|
45
46
|
* promotion or demotion cannot apply on one code path and not another.
|
|
46
47
|
*/
|
|
47
48
|
private criticality;
|
|
49
|
+
/** The verdict's six defaults, resolved once from the config this engine was built with. */
|
|
50
|
+
private compose;
|
|
48
51
|
/**
|
|
49
52
|
* `criticalityOverrides` are `config.eval` — the criticalRules /
|
|
50
53
|
* nonCriticalRules lists. Validated here as well as in loadConfig, so an
|
|
@@ -52,6 +55,7 @@ export declare class EvalEngine {
|
|
|
52
55
|
* misspelled rule name.
|
|
53
56
|
*/
|
|
54
57
|
constructor(threshold?: number, ruleThresholds?: Record<string, unknown>, criticalityOverrides?: CriticalityOverrides);
|
|
58
|
+
private decide;
|
|
55
59
|
/** The effective criticality of one rule under this engine's config. Read by the rule roster surfaces. */
|
|
56
60
|
effectiveCriticality(rule: EvalRule): EffectiveCriticality;
|
|
57
61
|
/**
|
|
@@ -71,7 +75,7 @@ export declare class EvalEngine {
|
|
|
71
75
|
unregisterRule(ruleId: string): boolean;
|
|
72
76
|
/** Whether a deployed rule id is currently registered (and therefore firing). */
|
|
73
77
|
hasRule(ruleId: string): boolean;
|
|
74
|
-
evaluate(evalType: EvalType, context: EvalContext, customRules?: CustomRuleDefinition[]): EvalResult
|
|
78
|
+
evaluate(evalType: EvalType, context: EvalContext, customRules?: CustomRuleDefinition[]): Promise<EvalResult>;
|
|
75
79
|
/**
|
|
76
80
|
* eval_type="all" (#370): every built-in bundle, each with the deployed
|
|
77
81
|
* rules registered under it, plus the rules deployed under "custom" and
|
|
@@ -81,7 +85,7 @@ export declare class EvalEngine {
|
|
|
81
85
|
* that ran (weighted score against the threshold, critical veto across
|
|
82
86
|
* all bundles); `categories` carries the same arithmetic per bundle.
|
|
83
87
|
*/
|
|
84
|
-
evaluateAll(context: EvalContext, customRules?: CustomRuleDefinition[]): EvalResult
|
|
88
|
+
evaluateAll(context: EvalContext, customRules?: CustomRuleDefinition[]): Promise<EvalResult>;
|
|
85
89
|
private run;
|
|
86
90
|
/**
|
|
87
91
|
* Weighted average over the rules that ran, plus the critical veto.
|
package/dist/eval/engine.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { getRulesForType, createCustomRule } from './rules/index.js';
|
|
2
2
|
import { criticalityResolver } from './criticality.js';
|
|
3
|
+
import { compose, interpretations, DEFAULT_COMPOSE } from './compose.js';
|
|
4
|
+
import { inputsPresent, stampRuleResult } from './stamp.js';
|
|
5
|
+
import { buildProvenance, configHash, deriveCoverage, deriveVerdict, rulesetHash } from './verdict.js';
|
|
6
|
+
import { PKG_VERSION } from '../config/defaults.js';
|
|
3
7
|
import { generateEvalId } from '../utils/ids.js';
|
|
4
8
|
/**
|
|
5
9
|
* Every bundle eval_type="all" walks, in the order their categories are
|
|
@@ -40,12 +44,15 @@ export class EvalEngine {
|
|
|
40
44
|
idByRule = new Map();
|
|
41
45
|
threshold;
|
|
42
46
|
ruleThresholds;
|
|
47
|
+
criticalityOverrides;
|
|
43
48
|
/**
|
|
44
49
|
* Effective criticality per rule, bound to this engine's config overrides.
|
|
45
50
|
* Every veto decision reads THIS, never `rule.critical` directly, so a
|
|
46
51
|
* promotion or demotion cannot apply on one code path and not another.
|
|
47
52
|
*/
|
|
48
53
|
criticality;
|
|
54
|
+
/** The verdict's six defaults, resolved once from the config this engine was built with. */
|
|
55
|
+
compose;
|
|
49
56
|
/**
|
|
50
57
|
* `criticalityOverrides` are `config.eval` — the criticalRules /
|
|
51
58
|
* nonCriticalRules lists. Validated here as well as in loadConfig, so an
|
|
@@ -55,7 +62,35 @@ export class EvalEngine {
|
|
|
55
62
|
constructor(threshold = 0.7, ruleThresholds, criticalityOverrides) {
|
|
56
63
|
this.threshold = threshold;
|
|
57
64
|
this.ruleThresholds = ruleThresholds;
|
|
65
|
+
this.criticalityOverrides = criticalityOverrides;
|
|
58
66
|
this.criticality = criticalityResolver(criticalityOverrides);
|
|
67
|
+
this.compose = {
|
|
68
|
+
composer: criticalityOverrides?.composer ?? DEFAULT_COMPOSE.composer,
|
|
69
|
+
falsePassCost: criticalityOverrides?.falsePassCost ?? DEFAULT_COMPOSE.falsePassCost,
|
|
70
|
+
onCriticalSkipped: criticalityOverrides?.onCriticalSkipped ?? DEFAULT_COMPOSE.onCriticalSkipped,
|
|
71
|
+
requiredEvidence: criticalityOverrides?.requiredEvidence ?? DEFAULT_COMPOSE.requiredEvidence,
|
|
72
|
+
defaultsGate: criticalityOverrides?.defaultsGate ?? DEFAULT_COMPOSE.defaultsGate,
|
|
73
|
+
prior: criticalityOverrides?.prior ?? DEFAULT_COMPOSE.prior,
|
|
74
|
+
priorMode: criticalityOverrides?.priorMode ?? DEFAULT_COMPOSE.priorMode,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* The verdict, and `passed` with it.
|
|
79
|
+
*
|
|
80
|
+
* From 0.10.0 `passed` IS `verdict.state === 'pass'` — one definition, on
|
|
81
|
+
* every surface. The weighted `score` survives untouched as a quality
|
|
82
|
+
* gradient over the rules that ran, and is never re-meant: a reader who
|
|
83
|
+
* was using it as a gradient keeps it, and a reader who was using it as a
|
|
84
|
+
* safety signal was reading a number that arc zero measured as inert.
|
|
85
|
+
*/
|
|
86
|
+
decide(result) {
|
|
87
|
+
const verdict = this.compose.composer === 'legacy' ? deriveVerdict(result, this.threshold) : compose(result, this.compose);
|
|
88
|
+
result.verdict = verdict;
|
|
89
|
+
result.passed = verdict.passed;
|
|
90
|
+
const notes = interpretations(result, verdict, this.compose);
|
|
91
|
+
if (notes.length > 0)
|
|
92
|
+
result.interpretations = notes;
|
|
93
|
+
return result;
|
|
59
94
|
}
|
|
60
95
|
/** The effective criticality of one rule under this engine's config. Read by the rule roster surfaces. */
|
|
61
96
|
effectiveCriticality(rule) {
|
|
@@ -104,7 +139,7 @@ export class EvalEngine {
|
|
|
104
139
|
hasRule(ruleId) {
|
|
105
140
|
return this.rulesById.has(ruleId);
|
|
106
141
|
}
|
|
107
|
-
evaluate(evalType, context, customRules) {
|
|
142
|
+
async evaluate(evalType, context, customRules) {
|
|
108
143
|
/*
|
|
109
144
|
* Inline custom_rules are ADDITIVE, which is what evaluate_output's
|
|
110
145
|
* description promises in two places: "fires REGARDLESS of eval_type"
|
|
@@ -141,7 +176,7 @@ export class EvalEngine {
|
|
|
141
176
|
* that ran (weighted score against the threshold, critical veto across
|
|
142
177
|
* all bundles); `categories` carries the same arithmetic per bundle.
|
|
143
178
|
*/
|
|
144
|
-
evaluateAll(context, customRules) {
|
|
179
|
+
async evaluateAll(context, customRules) {
|
|
145
180
|
const rules = [];
|
|
146
181
|
const categories = [];
|
|
147
182
|
for (const type of ALL_EVAL_TYPES) {
|
|
@@ -156,7 +191,16 @@ export class EvalEngine {
|
|
|
156
191
|
}
|
|
157
192
|
return this.run('all', rules, categories, context);
|
|
158
193
|
}
|
|
159
|
-
|
|
194
|
+
/*
|
|
195
|
+
* Async from 0.10.0. Nothing it awaits yet: every rule the package ships
|
|
196
|
+
* is synchronous, and `EvalRule.evaluate` stays synchronous so the type
|
|
197
|
+
* system keeps proving that a deterministic rule cannot reach the network
|
|
198
|
+
* — which is what makes "evaluate_output never spends" a compile-time
|
|
199
|
+
* fact rather than a test. The signature moves first, in one mechanical
|
|
200
|
+
* change, so the judgment rule that DOES call a provider can be added
|
|
201
|
+
* without re-touching every caller a second time.
|
|
202
|
+
*/
|
|
203
|
+
async run(evalType, rules, categories, context) {
|
|
160
204
|
// Merge system-level thresholds into customConfig (user-provided values take precedence)
|
|
161
205
|
if (this.ruleThresholds) {
|
|
162
206
|
context = {
|
|
@@ -177,6 +221,7 @@ export class EvalEngine {
|
|
|
177
221
|
rules_evaluated: 0,
|
|
178
222
|
rules_skipped: 0,
|
|
179
223
|
insufficient_data: true,
|
|
224
|
+
verdict: { state: 'unknown', passed: false, basis: 'no_rules', by: [], risk: null },
|
|
180
225
|
};
|
|
181
226
|
}
|
|
182
227
|
/*
|
|
@@ -188,10 +233,41 @@ export class EvalEngine {
|
|
|
188
233
|
* rule it carries.
|
|
189
234
|
*/
|
|
190
235
|
const evalContext = { ...context, regexBudget: { breaches: 0 } };
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
/*
|
|
237
|
+
* Sequential, and it must stay sequential when a rule becomes awaitable:
|
|
238
|
+
* every rule in one evaluation shares the regex circuit breaker above,
|
|
239
|
+
* and running them concurrently would race the breach count that bounds
|
|
240
|
+
* a hostile output.
|
|
241
|
+
*/
|
|
242
|
+
const ruleResults = [];
|
|
243
|
+
for (const [i, rule] of rules.entries()) {
|
|
244
|
+
/*
|
|
245
|
+
* A judgment rule calls a provider and costs money. It runs only when
|
|
246
|
+
* the caller has said this evaluation may spend — which the free
|
|
247
|
+
* evaluation path never does. Enforced here, on the one path every
|
|
248
|
+
* evaluation takes, so no tool can forget it and no future rule can
|
|
249
|
+
* quietly opt itself in.
|
|
250
|
+
*/
|
|
251
|
+
const raw = rule.kind === 'judgment' && evalContext.allowPaid !== true
|
|
252
|
+
? {
|
|
253
|
+
ruleName: rule.name,
|
|
254
|
+
passed: false,
|
|
255
|
+
score: 0,
|
|
256
|
+
message: 'Judgment rules are not run on this path: it may not call a paid provider.',
|
|
257
|
+
skipped: true,
|
|
258
|
+
skipReason: 'this evaluation may not spend (context.allowPaid is not set)',
|
|
259
|
+
}
|
|
260
|
+
: rule.evaluate(evalContext);
|
|
193
261
|
const ruleId = this.idByRule.get(rule);
|
|
194
|
-
|
|
262
|
+
/*
|
|
263
|
+
* The bundle this rule ran under. `categories` is only supplied for
|
|
264
|
+
* eval_type="all"; for a single bundle the rule's own evalType is the
|
|
265
|
+
* answer and is just as true. It used to be left off, which meant a
|
|
266
|
+
* single-bundle call could not tell a custom rule from a built-in one
|
|
267
|
+
* — and the composer needs that, because a custom rule's severity is
|
|
268
|
+
* the deployment's own statement of how much it matters.
|
|
269
|
+
*/
|
|
270
|
+
const category = categories?.[i] ?? (evalType === 'all' ? rule.evalType : evalType);
|
|
195
271
|
/*
|
|
196
272
|
* Every result says whether THIS rule vetoes and who decided that.
|
|
197
273
|
* Without it, a reader holding a failed evaluation cannot tell a
|
|
@@ -201,21 +277,53 @@ export class EvalEngine {
|
|
|
201
277
|
* the one path every evaluation takes, so a surface cannot render the
|
|
202
278
|
* declared criticality where the engine applied a configured one.
|
|
203
279
|
*/
|
|
204
|
-
const
|
|
280
|
+
const effective = this.criticality(rule);
|
|
281
|
+
const { critical, source } = effective;
|
|
205
282
|
// ruleId / category sit right after the name so a reader scanning
|
|
206
283
|
// rule_results sees WHICH deployed rule (and which bundle) spoke.
|
|
207
284
|
const { ruleName, ...rest } = raw;
|
|
208
|
-
|
|
285
|
+
/*
|
|
286
|
+
* The stamp (0.9.0): what kind of claim this is, what the composer
|
|
287
|
+
* did with it, which question it answers, what it saw, why it skipped,
|
|
288
|
+
* and how wrong it tends to be — from the rule's declaration, the
|
|
289
|
+
* inputs this call carried, and the published accuracy that ships in
|
|
290
|
+
* the package. Computed here, on the one path every evaluation takes,
|
|
291
|
+
* so no surface can show a result without its receipt. It changes no
|
|
292
|
+
* verdict: summarize() below still decides passed exactly as before.
|
|
293
|
+
*/
|
|
294
|
+
ruleResults.push({
|
|
209
295
|
ruleName,
|
|
210
296
|
...(ruleId !== undefined ? { ruleId } : {}),
|
|
211
297
|
...(category !== undefined ? { category } : {}),
|
|
212
298
|
critical,
|
|
213
299
|
criticalSource: source,
|
|
214
300
|
...rest,
|
|
215
|
-
|
|
216
|
-
|
|
301
|
+
...stampRuleResult(rule, raw, context, effective),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
217
304
|
const overall = this.summarize(rules, ruleResults);
|
|
218
305
|
const perCategory = categories ? this.categorize(rules, ruleResults, categories) : undefined;
|
|
306
|
+
/*
|
|
307
|
+
* The receipt for the whole evaluation (0.9.0): what produced it, which
|
|
308
|
+
* questions it judged, and the basis of its verdict. Computed here from
|
|
309
|
+
* what the engine already holds; persisted as provenance and derived
|
|
310
|
+
* again on every read, so a stored row answers "why did this pass on
|
|
311
|
+
* that day" without a backfill. Changes no verdict.
|
|
312
|
+
*/
|
|
313
|
+
const provenance = buildProvenance({
|
|
314
|
+
irisVersion: PKG_VERSION,
|
|
315
|
+
rulesetHash: rulesetHash(rules, (r) => this.criticality(r)),
|
|
316
|
+
configHash: configHash({
|
|
317
|
+
threshold: this.threshold,
|
|
318
|
+
ruleThresholds: this.ruleThresholds,
|
|
319
|
+
criticalRules: this.criticalityOverrides?.criticalRules,
|
|
320
|
+
nonCriticalRules: this.criticalityOverrides?.nonCriticalRules,
|
|
321
|
+
}),
|
|
322
|
+
threshold: this.threshold,
|
|
323
|
+
ruleThresholds: this.ruleThresholds,
|
|
324
|
+
judgedAt: new Date().toISOString(),
|
|
325
|
+
});
|
|
326
|
+
const coverage = deriveCoverage(ruleResults, inputsPresent(context));
|
|
219
327
|
// Handle "all rules skipped" — insufficient data
|
|
220
328
|
if (overall.rulesEvaluated === 0) {
|
|
221
329
|
const skipMessages = ruleResults
|
|
@@ -225,7 +333,7 @@ export class EvalEngine {
|
|
|
225
333
|
// that EVERY critical rule that skipped is named here, and a caller
|
|
226
334
|
// whose only rules were critical ones should not have to infer that
|
|
227
335
|
// from insufficient_data alone.
|
|
228
|
-
|
|
336
|
+
const unknown = {
|
|
229
337
|
id: generateEvalId(),
|
|
230
338
|
eval_type: evalType,
|
|
231
339
|
output_text: context.output,
|
|
@@ -242,7 +350,10 @@ export class EvalEngine {
|
|
|
242
350
|
insufficient_data: true,
|
|
243
351
|
...(overall.criticalSkipped.length > 0 ? { critical_skipped: overall.criticalSkipped } : {}),
|
|
244
352
|
...(perCategory ? { categories: perCategory } : {}),
|
|
353
|
+
coverage,
|
|
354
|
+
provenance,
|
|
245
355
|
};
|
|
356
|
+
return this.decide(unknown);
|
|
246
357
|
}
|
|
247
358
|
const suggestions = [];
|
|
248
359
|
for (const result of ruleResults) {
|
|
@@ -273,7 +384,7 @@ export class EvalEngine {
|
|
|
273
384
|
'checks, not "clean"; a gate that must fail closed should treat critical_skipped ' +
|
|
274
385
|
'as a failure.');
|
|
275
386
|
}
|
|
276
|
-
|
|
387
|
+
const result = {
|
|
277
388
|
id: generateEvalId(),
|
|
278
389
|
eval_type: evalType,
|
|
279
390
|
output_text: context.output,
|
|
@@ -288,7 +399,10 @@ export class EvalEngine {
|
|
|
288
399
|
...(overall.criticalFailures.length > 0 ? { critical_failures: overall.criticalFailures } : {}),
|
|
289
400
|
...(overall.criticalSkipped.length > 0 ? { critical_skipped: overall.criticalSkipped } : {}),
|
|
290
401
|
...(perCategory ? { categories: perCategory } : {}),
|
|
402
|
+
coverage,
|
|
403
|
+
provenance,
|
|
291
404
|
};
|
|
405
|
+
return this.decide(result);
|
|
292
406
|
}
|
|
293
407
|
/**
|
|
294
408
|
* Weighted average over the rules that ran, plus the critical veto.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FailureClass, Need } from '../types/eval.js';
|
|
2
|
+
export interface FailureClassEntry {
|
|
3
|
+
id: FailureClass;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const FAILURE_CLASSES: readonly FailureClassEntry[];
|
|
7
|
+
export declare const FAILURE_CLASS_IDS: readonly FailureClass[];
|
|
8
|
+
export declare const NEEDS: readonly Need[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const FAILURE_CLASSES = [
|
|
2
|
+
{ id: 'pii_leak', text: 'Personal data in the output (SSN, card, phone, email, date of birth, address …)' },
|
|
3
|
+
{ id: 'credential_leak', text: 'A secret in the output (API key, token, private key, seed phrase)' },
|
|
4
|
+
{ id: 'injection', text: 'Injection-shaped content in the output: attack phrasing or a directive aimed at an evaluator or a downstream system' },
|
|
5
|
+
{ id: 'injection_compliance', text: 'The agent read an injected instruction in a tool result and obeyed it' },
|
|
6
|
+
{ id: 'silent_tool_failure', text: 'A tool call failed and the output never acknowledges it' },
|
|
7
|
+
{ id: 'tool_loop', text: 'The same call repeated past the configured limit with the same result' },
|
|
8
|
+
{ id: 'stub', text: 'A placeholder, a deferral or a promise instead of the work' },
|
|
9
|
+
{ id: 'fabrication', text: 'A claim that contradicts the material the agent was given' },
|
|
10
|
+
{ id: 'ungrounded', text: 'An identifier, number or citation in the output that nothing the agent read supports' },
|
|
11
|
+
{ id: 'incomplete_ask', text: 'Part of a multi-part ask was not answered' },
|
|
12
|
+
{ id: 'off_task', text: 'The output does not address what was asked' },
|
|
13
|
+
{ id: 'over_budget', text: 'The run cost more than the deployment allows, in money or tokens' },
|
|
14
|
+
{ id: 'format', text: 'The output fails a shape or size requirement (empty, too short, too few sentences, invalid JSON)' },
|
|
15
|
+
{ id: 'invalid_tool_call', text: 'A tool was called that is not in the catalogue, or with arguments its schema rejects' },
|
|
16
|
+
];
|
|
17
|
+
export const FAILURE_CLASS_IDS = FAILURE_CLASSES.map((c) => c.id);
|
|
18
|
+
export const NEEDS = ['output', 'input', 'expected', 'tool_calls', 'tool_outputs', 'tools_catalogue', 'cost', 'tokens', 'citations'];
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { type LLMProvider } from './client.js';
|
|
2
2
|
import { type TemplateName } from './templates/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The pre-check refused the call: the worst-case spend (two attempts) would
|
|
5
|
+
* exceed the cap. Typed so the tool can answer IRIS_BUDGET_EXCEEDED with both
|
|
6
|
+
* numbers; nothing was spent.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CostCapError extends Error {
|
|
9
|
+
readonly estimatedUsd: number;
|
|
10
|
+
readonly capUsd: number;
|
|
11
|
+
constructor(estimatedUsd: number, capUsd: number);
|
|
12
|
+
}
|
|
3
13
|
export interface LLMJudgeEvaluateParams {
|
|
4
14
|
output: string;
|
|
5
15
|
template: TemplateName;
|
|
@@ -16,8 +26,28 @@ export interface LLMJudgeEvaluateParams {
|
|
|
16
26
|
maxInputTokensEstimate?: number;
|
|
17
27
|
}
|
|
18
28
|
export interface LLMJudgeEvaluationResult {
|
|
29
|
+
/**
|
|
30
|
+
* The verdict, and it is the THRESHOLD's, not the model's.
|
|
31
|
+
*
|
|
32
|
+
* Until 0.10.0 the model's own `passed` boolean won whenever it supplied
|
|
33
|
+
* one, and the template's threshold was a fallback the product rarely
|
|
34
|
+
* reached. That let a judge return `score: 0.2` with `passed: true` and
|
|
35
|
+
* be believed — a scoring rubric whose score did not decide anything.
|
|
36
|
+
* Now the score is the measurement and the threshold is the rule.
|
|
37
|
+
*/
|
|
19
38
|
passed: boolean;
|
|
20
39
|
score: number;
|
|
40
|
+
/** The threshold the score was read against, so a reader can check the arithmetic. */
|
|
41
|
+
passThreshold: number;
|
|
42
|
+
/** What the model said about passing, when it said anything. Recorded, never obeyed. */
|
|
43
|
+
selfReportedPass?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* True when the model's own boolean disagrees with the threshold verdict.
|
|
46
|
+
* Worth surfacing: a judge that scores 0.95 and says "fail", or scores
|
|
47
|
+
* 0.2 and says "pass", is telling you its rubric and its judgement have
|
|
48
|
+
* come apart on this output.
|
|
49
|
+
*/
|
|
50
|
+
disagreement?: boolean;
|
|
21
51
|
rationale: string;
|
|
22
52
|
dimensions: Record<string, number>;
|
|
23
53
|
model: string;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { callLLMJudge, estimateInputTokens, LLMJudgeError } from './client.js';
|
|
2
2
|
import { estimateCostUsd, findPricing } from './pricing.js';
|
|
3
3
|
import { getTemplate } from './templates/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The pre-check refused the call: the worst-case spend (two attempts) would
|
|
6
|
+
* exceed the cap. Typed so the tool can answer IRIS_BUDGET_EXCEEDED with both
|
|
7
|
+
* numbers; nothing was spent.
|
|
8
|
+
*/
|
|
9
|
+
export class CostCapError extends Error {
|
|
10
|
+
estimatedUsd;
|
|
11
|
+
capUsd;
|
|
12
|
+
constructor(estimatedUsd, capUsd) {
|
|
13
|
+
super(`Estimated max cost ${estimatedUsd.toFixed(4)} USD (including one retry on a malformed judge reply) exceeds cap ${capUsd.toFixed(4)} USD — refusing to call. Raise IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or max_cost_usd, or trim prompts/maxOutputTokens.`);
|
|
14
|
+
this.estimatedUsd = estimatedUsd;
|
|
15
|
+
this.capUsd = capUsd;
|
|
16
|
+
this.name = 'CostCapError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
4
19
|
// Malformed judge response — retried once by `evaluate`, surfaced as
|
|
5
20
|
// LLMJudgeError('malformed_response') if the retry also fails.
|
|
6
21
|
function parseJudgeResponse(raw) {
|
|
@@ -88,7 +103,7 @@ export async function evaluateWithLLMJudge(params) {
|
|
|
88
103
|
const retryCost = estimateCostUsd(params.model, estimateInputTokens(strictSystem, userPrompt), retryMaxOutputTokens);
|
|
89
104
|
const estimatedCost = firstAttemptCost === null || retryCost === null ? null : firstAttemptCost + retryCost;
|
|
90
105
|
if (estimatedCost !== null && estimatedCost > maxCost) {
|
|
91
|
-
throw new
|
|
106
|
+
throw new CostCapError(estimatedCost, maxCost);
|
|
92
107
|
}
|
|
93
108
|
// First attempt
|
|
94
109
|
let raw = await callLLMJudge({
|
|
@@ -136,10 +151,19 @@ export async function evaluateWithLLMJudge(params) {
|
|
|
136
151
|
latencyMs += raw.latencyMs;
|
|
137
152
|
parsed = parseJudgeResponse(raw.content);
|
|
138
153
|
}
|
|
139
|
-
|
|
154
|
+
/*
|
|
155
|
+
* The threshold decides. The model's own boolean is evidence about the
|
|
156
|
+
* model, not about the output, and it is recorded beside the verdict
|
|
157
|
+
* rather than substituted for it.
|
|
158
|
+
*/
|
|
159
|
+
const passed = parsed.score >= template.passThreshold;
|
|
160
|
+
const disagreement = parsed.passed !== undefined && parsed.passed !== passed;
|
|
140
161
|
const costUsd = estimateCostUsd(params.model, inputTokens, outputTokens);
|
|
141
162
|
return {
|
|
142
163
|
passed,
|
|
164
|
+
passThreshold: template.passThreshold,
|
|
165
|
+
...(parsed.passed !== undefined ? { selfReportedPass: parsed.passed } : {}),
|
|
166
|
+
...(disagreement ? { disagreement: true } : {}),
|
|
143
167
|
score: parsed.score,
|
|
144
168
|
rationale: parsed.rationale,
|
|
145
169
|
dimensions: parsed.dimensions,
|