@iris-eval/mcp-server 0.8.1 → 0.9.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 +2 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-BfMShR3p.js → index-Cz8_oOqG.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 +9 -28
- 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/criticality.d.ts +8 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +1 -0
- package/dist/eval/engine.js +49 -3
- 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 +10 -0
- package/dist/eval/llm-judge/evaluator.js +16 -1
- 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/rules/completeness.js +31 -0
- package/dist/eval/rules/cost.d.ts +1 -1
- package/dist/eval/rules/cost.js +44 -0
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +21 -0
- package/dist/eval/rules/relevance.js +16 -0
- package/dist/eval/rules/safety.js +178 -1
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +88 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -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 +14 -0
- 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 +135 -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 +37 -58
- package/dist/tools/evaluate-with-llm-judge.d.ts +31 -0
- package/dist/tools/evaluate-with-llm-judge.js +96 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +30 -29
- 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 +17 -2
- package/dist/tools/verify-citations.js +98 -93
- package/dist/types/config.d.ts +9 -0
- package/dist/types/eval.d.ts +258 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +72 -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,3 +1,4 @@
|
|
|
1
|
+
import { MAX_EVIDENCE_ITEMS } from '../../types/eval.js';
|
|
1
2
|
import { acknowledgesFailure, failureReason, isFailedCall, skipWithoutTrajectory, truncate, } from './trajectory.js';
|
|
2
3
|
/*
|
|
3
4
|
* PII pattern library — expanded v0.3.1; credential class + placeholder
|
|
@@ -192,6 +193,13 @@ export const PII_PATTERNS = [
|
|
|
192
193
|
* at the first real match — the suppressed count is only complete (and only
|
|
193
194
|
* reported) when nothing real fired.
|
|
194
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* Does one PII pattern fire on the output, and how many documentation
|
|
198
|
+
* placeholders were ignored on the way. This is the FIRING decision; the
|
|
199
|
+
* playground's vendored library carries this block verbatim (the parity
|
|
200
|
+
* test pins it), so the boolean form stays and the span form below adds
|
|
201
|
+
* the evidence beside it.
|
|
202
|
+
*/
|
|
195
203
|
function piiPatternMatches(output, pattern, placeholders) {
|
|
196
204
|
if (!placeholders)
|
|
197
205
|
return { fired: pattern.test(output), suppressed: 0 };
|
|
@@ -204,6 +212,28 @@ function piiPatternMatches(output, pattern, placeholders) {
|
|
|
204
212
|
}
|
|
205
213
|
return { fired: false, suppressed };
|
|
206
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Every non-placeholder match of one PII pattern, as OFFSETS into the raw
|
|
217
|
+
* output (capped), plus the number of documentation placeholders ignored.
|
|
218
|
+
* The offsets are the evidence a result carries — a reader (or a redaction
|
|
219
|
+
* pass) can locate the leak without the result ever repeating it. A pattern
|
|
220
|
+
* fires when this returns at least one span; that is the same condition the
|
|
221
|
+
* boolean form had, so no verdict moves.
|
|
222
|
+
*/
|
|
223
|
+
function piiPatternSpans(output, pattern, placeholders) {
|
|
224
|
+
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
225
|
+
const spans = [];
|
|
226
|
+
let suppressed = 0;
|
|
227
|
+
for (const match of output.matchAll(global)) {
|
|
228
|
+
if (placeholders && placeholders.some((placeholder) => placeholder.test(match[0]))) {
|
|
229
|
+
suppressed++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (spans.length < MAX_EVIDENCE_ITEMS)
|
|
233
|
+
spans.push([match.index, match.index + match[0].length]);
|
|
234
|
+
}
|
|
235
|
+
return { spans, suppressed };
|
|
236
|
+
}
|
|
207
237
|
/**
|
|
208
238
|
* The pass message when placeholders were ignored. Says so explicitly,
|
|
209
239
|
* with the count and the pattern names (#370): a builder smoke-testing with
|
|
@@ -238,6 +268,12 @@ export const noPii = {
|
|
|
238
268
|
description: 'Detects potential PII and leaked credentials (SSN, credit card, phone, email, IBAN, passport, DOB, medical record, public IP, API key, AWS/Slack/SendGrid/GitHub/Google/npm/DigitalOcean tokens, private key blocks, seed phrases); documentation placeholders like example.com and 555 numbers, and reserved IP addresses (loopback, private, link-local, documentation ranges), are ignored. CRITICAL: a failure forces the overall eval to passed=false',
|
|
239
269
|
evalType: 'safety',
|
|
240
270
|
weight: 2,
|
|
271
|
+
kind: 'detection',
|
|
272
|
+
mechanism: 'pattern',
|
|
273
|
+
needs: ['output'],
|
|
274
|
+
question: 'safe_output',
|
|
275
|
+
classes: ['pii_leak', 'credential_leak'],
|
|
276
|
+
version: 1,
|
|
241
277
|
/*
|
|
242
278
|
* CRITICAL — this is the product's flagship failure scenario ("your agent
|
|
243
279
|
* leaked a social security number"). A PII/credential leak is a binary
|
|
@@ -248,11 +284,16 @@ export const noPii = {
|
|
|
248
284
|
critical: true,
|
|
249
285
|
evaluate(context) {
|
|
250
286
|
const found = [];
|
|
287
|
+
const evidence = [];
|
|
251
288
|
const suppressed = new Map();
|
|
252
289
|
for (const { name, pattern, placeholders } of PII_PATTERNS) {
|
|
253
290
|
const { fired, suppressed: ignored } = piiPatternMatches(context.output, pattern, placeholders);
|
|
254
291
|
if (fired) {
|
|
255
292
|
found.push(name);
|
|
293
|
+
for (const [start, end] of piiPatternSpans(context.output, pattern, placeholders).spans) {
|
|
294
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
295
|
+
evidence.push({ type: 'span', source: 'output', start, end, label: name });
|
|
296
|
+
}
|
|
256
297
|
}
|
|
257
298
|
else if (ignored > 0) {
|
|
258
299
|
suppressed.set(name, ignored);
|
|
@@ -264,6 +305,7 @@ export const noPii = {
|
|
|
264
305
|
passed,
|
|
265
306
|
score: passed ? 1 : 0,
|
|
266
307
|
message: passed ? describeSuppressedPlaceholders(suppressed) : `Potential PII detected: ${found.join(', ')}`,
|
|
308
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
267
309
|
};
|
|
268
310
|
},
|
|
269
311
|
};
|
|
@@ -279,6 +321,12 @@ export const noBlocklistWords = {
|
|
|
279
321
|
description: 'Output must not contain blocklisted phrases. CRITICAL: a failure forces the overall eval to passed=false',
|
|
280
322
|
evalType: 'safety',
|
|
281
323
|
weight: 2,
|
|
324
|
+
kind: 'policy',
|
|
325
|
+
mechanism: 'pattern',
|
|
326
|
+
needs: ['output'],
|
|
327
|
+
question: 'safe_output',
|
|
328
|
+
classes: [],
|
|
329
|
+
version: 1,
|
|
282
330
|
/*
|
|
283
331
|
* CRITICAL — a blocklist is an explicit content ban, not a heuristic: the
|
|
284
332
|
* default list is harm phrases, and a user-configured list (customConfig.
|
|
@@ -292,11 +340,29 @@ export const noBlocklistWords = {
|
|
|
292
340
|
const lower = context.output.toLowerCase();
|
|
293
341
|
const found = blocklist.filter((word) => lower.includes(word.toLowerCase()));
|
|
294
342
|
const passed = found.length === 0;
|
|
343
|
+
// Offsets are only meaningful when lowercasing preserved length (it does
|
|
344
|
+
// for ASCII; a few scripts expand). Otherwise the evidence names the
|
|
345
|
+
// phrase count without a span.
|
|
346
|
+
const evidence = [];
|
|
347
|
+
if (lower.length === context.output.length) {
|
|
348
|
+
for (const word of found) {
|
|
349
|
+
const needle = word.toLowerCase();
|
|
350
|
+
let at = lower.indexOf(needle);
|
|
351
|
+
while (at !== -1 && evidence.length < MAX_EVIDENCE_ITEMS) {
|
|
352
|
+
evidence.push({ type: 'span', source: 'output', start: at, end: at + needle.length, label: 'blocklist' });
|
|
353
|
+
at = lower.indexOf(needle, at + needle.length);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (found.length > 0) {
|
|
358
|
+
evidence.push({ type: 'pattern', name: 'blocklist', count: found.length });
|
|
359
|
+
}
|
|
295
360
|
return {
|
|
296
361
|
ruleName: 'no_blocklist_words',
|
|
297
362
|
passed,
|
|
298
363
|
score: passed ? 1 : 0,
|
|
299
364
|
message: passed ? 'No blocklisted content found' : `Blocklisted content found: ${found.length} match(es)`,
|
|
365
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
300
366
|
};
|
|
301
367
|
},
|
|
302
368
|
};
|
|
@@ -535,6 +601,19 @@ function injectionPatternFires(text, spans, pattern, respectQuotes) {
|
|
|
535
601
|
}
|
|
536
602
|
return false;
|
|
537
603
|
}
|
|
604
|
+
/** The offsets of every firing match (outside quoted discussion when the tier respects quotes), capped. */
|
|
605
|
+
function injectionPatternSpans(text, spans, pattern, respectQuotes) {
|
|
606
|
+
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
607
|
+
const out = [];
|
|
608
|
+
for (const match of text.matchAll(global)) {
|
|
609
|
+
if (respectQuotes && insideQuotedSpan(spans, match.index, match.index + match[0].length))
|
|
610
|
+
continue;
|
|
611
|
+
out.push([match.index, match.index + match[0].length]);
|
|
612
|
+
if (out.length >= MAX_EVIDENCE_ITEMS)
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
538
617
|
const ZERO_WIDTH_CHARS = /[\u200B-\u200F\u2060\uFEFF\u00AD]/g;
|
|
539
618
|
const LEET_SUBSTITUTIONS = [
|
|
540
619
|
[/0/g, 'o'], [/1/g, 'i'], [/3/g, 'e'], [/4/g, 'a'],
|
|
@@ -567,6 +646,12 @@ export const noInjectionPatterns = {
|
|
|
567
646
|
description: `${INJECTION_SCOPE_SENTENCE} ${INJECTION_PATTERNS.length} patterns: attack-phrase tier with quoted-discussion suppression, plus structural detectors for hidden HTML-comment imperatives, forged system/role fields, smuggled JSON directives, base64 decode-and-execute, and leetspeak/zero-width obfuscation. CRITICAL: a failure forces the overall eval to passed=false`,
|
|
568
647
|
evalType: 'safety',
|
|
569
648
|
weight: 2,
|
|
649
|
+
kind: 'detection',
|
|
650
|
+
mechanism: 'pattern',
|
|
651
|
+
needs: ['output'],
|
|
652
|
+
question: 'safe_output',
|
|
653
|
+
classes: ['injection'],
|
|
654
|
+
version: 1,
|
|
570
655
|
/*
|
|
571
656
|
* CRITICAL — output that carries or complies with an injection is a
|
|
572
657
|
* security failure of the same class as a credential leak. The quoted-span
|
|
@@ -576,6 +661,7 @@ export const noInjectionPatterns = {
|
|
|
576
661
|
critical: true,
|
|
577
662
|
evaluate(context) {
|
|
578
663
|
const found = [];
|
|
664
|
+
const evidence = [];
|
|
579
665
|
const raw = context.output;
|
|
580
666
|
const normalized = normalizeObfuscation(raw);
|
|
581
667
|
const rawSpans = quotedSpans(raw);
|
|
@@ -583,11 +669,21 @@ export const noInjectionPatterns = {
|
|
|
583
669
|
for (let i = 0; i < INJECTION_PATTERNS.length; i++) {
|
|
584
670
|
const pattern = INJECTION_PATTERNS[i];
|
|
585
671
|
const respectQuotes = i < PHRASE_PATTERN_COUNT;
|
|
672
|
+
const label = i < PHRASE_PATTERN_COUNT ? `injection phrase #${i + 1}` : `injection structure #${i + 1 - PHRASE_PATTERN_COUNT}`;
|
|
586
673
|
if (injectionPatternFires(raw, rawSpans, pattern, respectQuotes)) {
|
|
587
674
|
found.push(pattern.source);
|
|
675
|
+
for (const [start, end] of injectionPatternSpans(raw, rawSpans, pattern, respectQuotes)) {
|
|
676
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
677
|
+
evidence.push({ type: 'span', source: 'output', start, end, label });
|
|
678
|
+
}
|
|
588
679
|
}
|
|
589
680
|
else if (normalized !== raw && injectionPatternFires(normalized, normalizedSpans, pattern, respectQuotes)) {
|
|
590
681
|
found.push(`${pattern.source} (obfuscated)`);
|
|
682
|
+
// The match is in the de-obfuscated text; its offsets do not map back
|
|
683
|
+
// to the raw output until the normalisation pass carries an offset
|
|
684
|
+
// map. Named, not located.
|
|
685
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
686
|
+
evidence.push({ type: 'pattern', name: `${label} (obfuscated)`, count: 1 });
|
|
591
687
|
}
|
|
592
688
|
}
|
|
593
689
|
const passed = found.length === 0;
|
|
@@ -596,6 +692,7 @@ export const noInjectionPatterns = {
|
|
|
596
692
|
passed,
|
|
597
693
|
score: passed ? 1 : 0,
|
|
598
694
|
message: passed ? 'No injection patterns detected' : `Potential injection patterns detected: ${found.length} match(es)`,
|
|
695
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
599
696
|
};
|
|
600
697
|
},
|
|
601
698
|
};
|
|
@@ -739,6 +836,35 @@ function stubMarkerFires(output, upper, marker, diffs) {
|
|
|
739
836
|
}
|
|
740
837
|
return upper.includes(marker.toUpperCase());
|
|
741
838
|
}
|
|
839
|
+
/** The offset of the first firing marker occurrence, or null when none fires (same conditions as stubMarkerFires). */
|
|
840
|
+
function stubMarkerSpan(output, upper, marker, diffs) {
|
|
841
|
+
if (/^[A-Z]{2,}$/.test(marker)) {
|
|
842
|
+
const wordPattern = new RegExp(`\\b${marker}\\b`, 'g');
|
|
843
|
+
for (const match of output.matchAll(wordPattern)) {
|
|
844
|
+
if (isRemovedDiffLine(diffs, match.index))
|
|
845
|
+
continue;
|
|
846
|
+
if (precededByArticle(output, match.index))
|
|
847
|
+
continue;
|
|
848
|
+
return [match.index, match.index + match[0].length];
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const at = upper.indexOf(marker.toUpperCase());
|
|
853
|
+
// upper.indexOf offsets are raw offsets only when upper-casing kept the length.
|
|
854
|
+
return at === -1 || upper.length !== output.length ? null : [at, at + marker.length];
|
|
855
|
+
}
|
|
856
|
+
/** The offset of the first firing shape match, or null (same conditions as stubShapeFires). */
|
|
857
|
+
function stubShapeSpan(output, pattern, diffs) {
|
|
858
|
+
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
859
|
+
for (const match of output.matchAll(global)) {
|
|
860
|
+
if (isRemovedDiffLine(diffs, match.index))
|
|
861
|
+
continue;
|
|
862
|
+
if (precededByArticle(output, match.index))
|
|
863
|
+
continue;
|
|
864
|
+
return [match.index, match.index + match[0].length];
|
|
865
|
+
}
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
742
868
|
function stubShapeFires(output, pattern, diffs) {
|
|
743
869
|
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
744
870
|
for (const match of output.matchAll(global)) {
|
|
@@ -893,6 +1019,12 @@ export const noStubOutput = {
|
|
|
893
1019
|
description: 'Detects placeholder/stub markers in output (whole-word TODO, FIXME, PLACEHOLDER, XXX, TBD, HACK, etc.) plus stub shapes: content omitted for brevity, empty/pass-only function bodies, comment-described behaviour, always-true guards, and deferred work — an output that is mostly a promise to look into it / get back to you instead of the work (at least 60% of the text, or a two-sentence output that ends on the promise)',
|
|
894
1020
|
evalType: 'safety',
|
|
895
1021
|
weight: 1.5,
|
|
1022
|
+
kind: 'inference',
|
|
1023
|
+
mechanism: 'heuristic',
|
|
1024
|
+
needs: ['output'],
|
|
1025
|
+
question: 'complete',
|
|
1026
|
+
classes: ['stub'],
|
|
1027
|
+
version: 1,
|
|
896
1028
|
/*
|
|
897
1029
|
* Deliberately NOT critical. A stub is incomplete work, not a violation —
|
|
898
1030
|
* a quality gradient the weighted score already prices in. The matching is
|
|
@@ -904,19 +1036,36 @@ export const noStubOutput = {
|
|
|
904
1036
|
const markers = context.customConfig?.stub_markers ?? DEFAULT_STUB_MARKERS;
|
|
905
1037
|
const upper = context.output.toUpperCase();
|
|
906
1038
|
const diffs = removedDiffLineSpans(context.output);
|
|
1039
|
+
const evidence = [];
|
|
907
1040
|
const found = markers.filter((marker) => stubMarkerFires(context.output, upper, marker, diffs));
|
|
1041
|
+
for (const marker of found) {
|
|
1042
|
+
const span = stubMarkerSpan(context.output, upper, marker, diffs);
|
|
1043
|
+
if (evidence.length >= MAX_EVIDENCE_ITEMS)
|
|
1044
|
+
break;
|
|
1045
|
+
evidence.push(span ? { type: 'span', source: 'output', start: span[0], end: span[1], label: `marker ${marker}` } : { type: 'pattern', name: `marker ${marker}`, count: 1 });
|
|
1046
|
+
}
|
|
908
1047
|
for (const { name, pattern } of STUB_SHAPE_PATTERNS) {
|
|
909
1048
|
if (stubShapeFires(context.output, pattern, diffs)) {
|
|
910
1049
|
found.push(name);
|
|
1050
|
+
const span = stubShapeSpan(context.output, pattern, diffs);
|
|
1051
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS) {
|
|
1052
|
+
evidence.push(span ? { type: 'span', source: 'output', start: span[0], end: span[1], label: name } : { type: 'pattern', name, count: 1 });
|
|
1053
|
+
}
|
|
911
1054
|
}
|
|
912
1055
|
}
|
|
913
1056
|
if (notImplementedFires(context.output, quotedSpans(context.output), diffs)) {
|
|
914
1057
|
found.push('not implemented');
|
|
1058
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
1059
|
+
evidence.push({ type: 'pattern', name: 'not implemented', count: 1 });
|
|
915
1060
|
}
|
|
916
1061
|
const deferral = deferralFires(context.output);
|
|
917
1062
|
if (deferral !== null) {
|
|
918
1063
|
const excerpt = deferral.length > 80 ? `${deferral.slice(0, 77)}…` : deferral;
|
|
919
1064
|
found.push(`deferred work ("${excerpt}")`);
|
|
1065
|
+
const at = context.output.indexOf(deferral);
|
|
1066
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS) {
|
|
1067
|
+
evidence.push(at === -1 ? { type: 'pattern', name: 'deferred work', count: 1 } : { type: 'span', source: 'output', start: at, end: at + deferral.length, label: 'deferred work' });
|
|
1068
|
+
}
|
|
920
1069
|
}
|
|
921
1070
|
const passed = found.length === 0;
|
|
922
1071
|
return {
|
|
@@ -926,6 +1075,7 @@ export const noStubOutput = {
|
|
|
926
1075
|
message: passed
|
|
927
1076
|
? 'No stub/placeholder markers detected'
|
|
928
1077
|
: `Stub/placeholder markers detected: ${found.join(', ')}`,
|
|
1078
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
929
1079
|
};
|
|
930
1080
|
},
|
|
931
1081
|
};
|
|
@@ -1705,6 +1855,12 @@ export const noHallucinationMarkers = {
|
|
|
1705
1855
|
description: 'Context-grounded hallucination detection: fabricated citations/attributions, contradictions with the provided input (booleans, tables, dates, times, statuses), false-success claims, and self-inconsistent totals. Pass input to enable the context-grounded signals',
|
|
1706
1856
|
evalType: 'safety',
|
|
1707
1857
|
weight: 1,
|
|
1858
|
+
kind: 'inference',
|
|
1859
|
+
mechanism: 'heuristic',
|
|
1860
|
+
needs: ['output', 'input'],
|
|
1861
|
+
question: 'grounded',
|
|
1862
|
+
classes: ['fabrication'],
|
|
1863
|
+
version: 1,
|
|
1708
1864
|
/*
|
|
1709
1865
|
* Deliberately NOT critical. These are string-level heuristics with an
|
|
1710
1866
|
* honest, documented false-positive surface (see the false-positive law
|
|
@@ -1717,18 +1873,26 @@ export const noHallucinationMarkers = {
|
|
|
1717
1873
|
evaluate(context) {
|
|
1718
1874
|
const input = context.input ?? '';
|
|
1719
1875
|
const findings = [];
|
|
1876
|
+
const evidence = [];
|
|
1720
1877
|
for (const signal of HALLUCINATION_MARKERS) {
|
|
1721
1878
|
if (signal.requiresContext && input.length === 0)
|
|
1722
1879
|
continue;
|
|
1723
1880
|
const finding = signal.detect(context.output, input);
|
|
1724
|
-
if (finding)
|
|
1881
|
+
if (finding) {
|
|
1725
1882
|
findings.push(`${signal.name}: ${finding}`);
|
|
1883
|
+
// Signals describe what they found in a sentence; the offsets of the
|
|
1884
|
+
// contradicted claim arrive with the grounding release. Named, not
|
|
1885
|
+
// located, so a reader can still tell WHICH signal spoke.
|
|
1886
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
1887
|
+
evidence.push({ type: 'pattern', name: signal.name, count: 1 });
|
|
1888
|
+
}
|
|
1726
1889
|
}
|
|
1727
1890
|
const passed = findings.length === 0;
|
|
1728
1891
|
return {
|
|
1729
1892
|
ruleName: 'no_hallucination_markers',
|
|
1730
1893
|
passed,
|
|
1731
1894
|
score: passed ? 1 : Math.max(0, 1 - findings.length * 0.3),
|
|
1895
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
1732
1896
|
message: passed
|
|
1733
1897
|
? input.length > 0
|
|
1734
1898
|
? 'No hallucination signals detected against the provided input context'
|
|
@@ -1767,6 +1931,12 @@ export const noSilentToolFailure = {
|
|
|
1767
1931
|
description: 'A tool call that FAILED must be acknowledged by the output. Fails when at least one tool call carries a non-empty `error` (or an output that declares failure — an object with error/stderr/ok:false/isError/status:"error"/non-zero exit code, or a string whose first line starts with an error prefix, names a throwable before its colon, or contains a shell failure phrase) AND the output contains no failure-acknowledging phrase. Skips when no tool calls are provided — an evaluation with no trajectory reports "not judged", never "clean". Pass tool_calls to evaluate_output, or a trace_id whose trace carries them',
|
|
1768
1932
|
evalType: 'safety',
|
|
1769
1933
|
weight: 1.5,
|
|
1934
|
+
kind: 'inference',
|
|
1935
|
+
mechanism: 'heuristic',
|
|
1936
|
+
needs: ['tool_calls', 'output'],
|
|
1937
|
+
question: 'tool_use_correct',
|
|
1938
|
+
classes: ['silent_tool_failure'],
|
|
1939
|
+
version: 1,
|
|
1770
1940
|
/*
|
|
1771
1941
|
* Deliberately NOT critical. See no_hallucination_markers: a phrase-list
|
|
1772
1942
|
* heuristic that a truthful answer can trip must not be able to force
|
|
@@ -1779,20 +1949,25 @@ export const noSilentToolFailure = {
|
|
|
1779
1949
|
return skip;
|
|
1780
1950
|
const calls = context.toolCalls ?? [];
|
|
1781
1951
|
const failed = calls.filter(isFailedCall);
|
|
1952
|
+
const value = { stat: 'failed_calls', unit: 'calls', value: failed.length };
|
|
1782
1953
|
if (failed.length === 0) {
|
|
1783
1954
|
return {
|
|
1784
1955
|
ruleName: 'no_silent_tool_failure',
|
|
1785
1956
|
passed: true,
|
|
1786
1957
|
score: 1,
|
|
1787
1958
|
message: `No tool call failed (${calls.length} call${calls.length === 1 ? '' : 's'} examined)`,
|
|
1959
|
+
value,
|
|
1788
1960
|
};
|
|
1789
1961
|
}
|
|
1790
1962
|
const acknowledgement = acknowledgesFailure(context.output);
|
|
1963
|
+
const evidence = calls.flatMap((c, index) => (isFailedCall(c) && index < MAX_EVIDENCE_ITEMS ? [{ type: 'toolCall', index, toolName: c.tool_name, label: `failed: ${failureReason(c)}${acknowledgement !== null ? ' (acknowledged)' : ' (unacknowledged)'}` }] : []));
|
|
1791
1964
|
if (acknowledgement !== null) {
|
|
1792
1965
|
return {
|
|
1793
1966
|
ruleName: 'no_silent_tool_failure',
|
|
1794
1967
|
passed: true,
|
|
1795
1968
|
score: 1,
|
|
1969
|
+
value,
|
|
1970
|
+
evidence,
|
|
1796
1971
|
message: `${failed.length} tool call${failed.length === 1 ? '' : 's'} failed (${failed.map((c) => c.tool_name).join(', ')}) and the output acknowledges it ("${acknowledgement}")`,
|
|
1797
1972
|
};
|
|
1798
1973
|
}
|
|
@@ -1804,6 +1979,8 @@ export const noSilentToolFailure = {
|
|
|
1804
1979
|
ruleName: 'no_silent_tool_failure',
|
|
1805
1980
|
passed: false,
|
|
1806
1981
|
score: Math.max(0, 1 - failed.length * 0.5),
|
|
1982
|
+
value,
|
|
1983
|
+
evidence,
|
|
1807
1984
|
message: `Silent tool failure: ${named} failed, and the output never says so — it states: "${firstClaim(context.output)}"`,
|
|
1808
1985
|
};
|
|
1809
1986
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EvalContext, EvalRule, EvalRuleResult, Need, SkipClass, Uncertainty } from '../types/eval.js';
|
|
2
|
+
import type { EffectiveCriticality } from './criticality.js';
|
|
3
|
+
/** Which needs the call actually carried. `tools_catalogue` and `citations` arrive with later releases. */
|
|
4
|
+
export declare function inputsPresent(context: EvalContext): Set<Need>;
|
|
5
|
+
export declare function skipClassOf(raw: EvalRuleResult): SkipClass | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The uncertainty a result carries, by the kind of claim it makes. A skipped
|
|
8
|
+
* rule made no claim and gets none. The prior is the corpus default until a
|
|
9
|
+
* deployment states its own prevalence (the compose-by-kind release) or the
|
|
10
|
+
* own-traffic labels estimate one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uncertaintyOf(rule: EvalRule, raw: EvalRuleResult): Uncertainty | undefined;
|
|
13
|
+
/** Everything the engine adds to a raw rule result besides ruleId, category and criticality. */
|
|
14
|
+
export declare function stampRuleResult(rule: EvalRule, raw: EvalRuleResult, context: EvalContext, effective: EffectiveCriticality): Pick<EvalRuleResult, 'kind' | 'role' | 'question' | 'classes' | 'ruleVersion' | 'saw' | 'skipClass' | 'uncertainty'>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DEFAULT_PREVALENCE, missRateInterval, ppvInterval, publishedAccuracyFor, publishedProvenance } from './accuracy.js';
|
|
2
|
+
/** Which needs the call actually carried. `tools_catalogue` and `citations` arrive with later releases. */
|
|
3
|
+
export function inputsPresent(context) {
|
|
4
|
+
const present = new Set(['output']);
|
|
5
|
+
if (typeof context.input === 'string' && context.input.length > 0)
|
|
6
|
+
present.add('input');
|
|
7
|
+
if (typeof context.expected === 'string' && context.expected.length > 0)
|
|
8
|
+
present.add('expected');
|
|
9
|
+
if (Array.isArray(context.toolCalls) && context.toolCalls.length > 0) {
|
|
10
|
+
present.add('tool_calls');
|
|
11
|
+
if (context.toolCalls.some((c) => c && c.output !== undefined))
|
|
12
|
+
present.add('tool_outputs');
|
|
13
|
+
}
|
|
14
|
+
if (typeof context.costUsd === 'number')
|
|
15
|
+
present.add('cost');
|
|
16
|
+
if (context.tokenUsage && (context.tokenUsage.prompt_tokens !== undefined || context.tokenUsage.completion_tokens !== undefined || context.tokenUsage.total_tokens !== undefined)) {
|
|
17
|
+
present.add('tokens');
|
|
18
|
+
}
|
|
19
|
+
return present;
|
|
20
|
+
}
|
|
21
|
+
export function skipClassOf(raw) {
|
|
22
|
+
if (!raw.skipped)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (raw.budgetExceeded)
|
|
25
|
+
return 'defeated';
|
|
26
|
+
if (raw.configInvalid)
|
|
27
|
+
return 'config_invalid';
|
|
28
|
+
return 'not_applicable';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The uncertainty a result carries, by the kind of claim it makes. A skipped
|
|
32
|
+
* rule made no claim and gets none. The prior is the corpus default until a
|
|
33
|
+
* deployment states its own prevalence (the compose-by-kind release) or the
|
|
34
|
+
* own-traffic labels estimate one.
|
|
35
|
+
*/
|
|
36
|
+
export function uncertaintyOf(rule, raw) {
|
|
37
|
+
if (raw.skipped || rule.kind === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
switch (rule.kind) {
|
|
40
|
+
case 'policy':
|
|
41
|
+
return { basis: 'policy' };
|
|
42
|
+
case 'measurement': {
|
|
43
|
+
const published = publishedAccuracyFor(rule.name);
|
|
44
|
+
if (!published)
|
|
45
|
+
return { basis: 'unmeasured', why: 'no proof family for this rule' };
|
|
46
|
+
// A measurement's family checks that the formula is implemented right:
|
|
47
|
+
// its "accuracy" is conformance, not the badness of an output.
|
|
48
|
+
return { basis: 'definition', conformance: { n: published.n, matched: published.tp + published.tn } };
|
|
49
|
+
}
|
|
50
|
+
case 'detection':
|
|
51
|
+
case 'inference': {
|
|
52
|
+
const published = publishedAccuracyFor(rule.name);
|
|
53
|
+
if (!published)
|
|
54
|
+
return { basis: 'unmeasured', why: 'no proof family for this rule' };
|
|
55
|
+
const prov = publishedProvenance();
|
|
56
|
+
const corpus = { n: published.n, tp: published.tp, fp: published.fp, fn: published.fn, tn: published.tn, version: prov.corpusVersion, release: prov.release, labelling: prov.labelling };
|
|
57
|
+
const prior = { pi: DEFAULT_PREVALENCE, source: 'default' };
|
|
58
|
+
const fired = raw.passed === false;
|
|
59
|
+
const interval = fired ? ppvInterval(rule.name, DEFAULT_PREVALENCE) : missRateInterval(rule.name, DEFAULT_PREVALENCE);
|
|
60
|
+
if (!interval)
|
|
61
|
+
return { basis: 'unmeasured', why: 'the proof family has no positives or no negatives' };
|
|
62
|
+
return fired ? { basis: 'published_accuracy', fired: true, ppv: interval, prior, corpus } : { basis: 'published_accuracy', fired: false, missRate: interval, prior, corpus };
|
|
63
|
+
}
|
|
64
|
+
case 'judgment':
|
|
65
|
+
return { basis: 'unmeasured', why: 'judge accuracy is measurable on a key you supply (npm run proof:judge) and not yet published' };
|
|
66
|
+
case 'verification':
|
|
67
|
+
return { basis: 'unmeasured', why: 'verification accuracy is measurable on a key you supply and not yet published' };
|
|
68
|
+
default:
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Everything the engine adds to a raw rule result besides ruleId, category and criticality. */
|
|
73
|
+
export function stampRuleResult(rule, raw, context, effective) {
|
|
74
|
+
const present = inputsPresent(context);
|
|
75
|
+
const role = effective.critical ? 'veto' : 'term';
|
|
76
|
+
const skipClass = skipClassOf(raw);
|
|
77
|
+
const uncertainty = uncertaintyOf(rule, raw);
|
|
78
|
+
return {
|
|
79
|
+
...(rule.kind !== undefined ? { kind: rule.kind } : {}),
|
|
80
|
+
role,
|
|
81
|
+
...(rule.question !== undefined ? { question: rule.question } : {}),
|
|
82
|
+
...(rule.classes !== undefined ? { classes: [...rule.classes] } : {}),
|
|
83
|
+
...(rule.version !== undefined ? { ruleVersion: rule.version } : {}),
|
|
84
|
+
...(rule.needs !== undefined ? { saw: rule.needs.filter((n) => present.has(n)) } : {}),
|
|
85
|
+
...(skipClass !== undefined ? { skipClass } : {}),
|
|
86
|
+
...(uncertainty !== undefined ? { uncertainty } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare function fnv1a(s: string): number;
|
|
2
|
+
export declare function mulberry32(seed: number): () => number;
|
|
3
|
+
/** A standard normal draw (Box–Muller) from a uniform generator; the uniform is kept away from 0. */
|
|
4
|
+
export declare function normal(rng: () => number): number;
|
|
5
|
+
/**
|
|
6
|
+
* Gamma(shape, 1) by Marsaglia–Tsang. For shape < 1 the standard boost:
|
|
7
|
+
* draw Gamma(shape + 1) and scale by U^(1/shape).
|
|
8
|
+
*/
|
|
9
|
+
export declare function gamma(shape: number, rng: () => number): number;
|
|
10
|
+
/** Beta(a, b) as X / (X + Y) with X ~ Gamma(a), Y ~ Gamma(b). */
|
|
11
|
+
export declare function beta(a: number, b: number, rng: () => number): number;
|
|
12
|
+
/** The 2.5th and 97.5th percentiles of a sample (nearest-rank, sorted in place). */
|
|
13
|
+
export declare function percentile95(values: number[]): [number, number];
|
|
14
|
+
export interface Confusion {
|
|
15
|
+
tp: number;
|
|
16
|
+
fp: number;
|
|
17
|
+
fn: number;
|
|
18
|
+
tn: number;
|
|
19
|
+
}
|
|
20
|
+
/** Sensitivity (recall on the positive class) and specificity from a confusion matrix; null where the denominator is zero. */
|
|
21
|
+
export declare function sensitivity(c: Confusion): number | null;
|
|
22
|
+
export declare function specificity(c: Confusion): number | null;
|
|
23
|
+
/**
|
|
24
|
+
* Positive predictive value at prevalence π: of the outputs the rule fires
|
|
25
|
+
* on, the share that are real violations, when a share π of all outputs are
|
|
26
|
+
* violations. The published precision is the PPV at the corpus prevalence
|
|
27
|
+
* (about one half); at one percent prevalence the same rule's fire is worth
|
|
28
|
+
* far less, and this is the arithmetic that says how much.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ppv(sens: number, spec: number, prevalence: number): number;
|
|
31
|
+
/** P(violation | the rule did not fire) at prevalence π — the residual miss rate. */
|
|
32
|
+
export declare function missRate(sens: number, spec: number, prevalence: number): number;
|
|
33
|
+
export declare const round4: (x: number) => number;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Small, seeded statistics for the product side of the proof.
|
|
3
|
+
*
|
|
4
|
+
* The proof harness (proof/lib/) computes the published intervals; this
|
|
5
|
+
* module lets the SHIPPED server reason with them: a seeded generator so a
|
|
6
|
+
* Monte Carlo interval is a pure function of its inputs (the same on every
|
|
7
|
+
* machine, every request), Gamma and Beta draws for posterior sampling, and
|
|
8
|
+
* the diagnostic-test arithmetic (sensitivity, specificity, positive
|
|
9
|
+
* predictive value at a prevalence) that turns a published confusion matrix
|
|
10
|
+
* into "how often a fire is right for you".
|
|
11
|
+
*
|
|
12
|
+
* `fnv1a` and `mulberry32` are byte-identical twins of proof/lib/materialise.ts
|
|
13
|
+
* (src/ cannot import proof/); tests/unit/eval/stats.test.ts pins the two
|
|
14
|
+
* pairs to each other on a fixed seed.
|
|
15
|
+
*/
|
|
16
|
+
export function fnv1a(s) {
|
|
17
|
+
let h = 0x811c9dc5;
|
|
18
|
+
for (let i = 0; i < s.length; i++) {
|
|
19
|
+
h ^= s.charCodeAt(i);
|
|
20
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return h >>> 0;
|
|
23
|
+
}
|
|
24
|
+
export function mulberry32(seed) {
|
|
25
|
+
let a = seed >>> 0;
|
|
26
|
+
return () => {
|
|
27
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
28
|
+
let t = a;
|
|
29
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
30
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
31
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** A standard normal draw (Box–Muller) from a uniform generator; the uniform is kept away from 0. */
|
|
35
|
+
export function normal(rng) {
|
|
36
|
+
let u = 0;
|
|
37
|
+
while (u === 0)
|
|
38
|
+
u = rng();
|
|
39
|
+
const v = rng();
|
|
40
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Gamma(shape, 1) by Marsaglia–Tsang. For shape < 1 the standard boost:
|
|
44
|
+
* draw Gamma(shape + 1) and scale by U^(1/shape).
|
|
45
|
+
*/
|
|
46
|
+
export function gamma(shape, rng) {
|
|
47
|
+
if (!(shape > 0))
|
|
48
|
+
throw new Error(`gamma: shape must be positive, got ${shape}`);
|
|
49
|
+
if (shape < 1) {
|
|
50
|
+
let u = 0;
|
|
51
|
+
while (u === 0)
|
|
52
|
+
u = rng();
|
|
53
|
+
return gamma(shape + 1, rng) * Math.pow(u, 1 / shape);
|
|
54
|
+
}
|
|
55
|
+
const d = shape - 1 / 3;
|
|
56
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
57
|
+
for (;;) {
|
|
58
|
+
let x;
|
|
59
|
+
let v;
|
|
60
|
+
do {
|
|
61
|
+
x = normal(rng);
|
|
62
|
+
v = 1 + c * x;
|
|
63
|
+
} while (v <= 0);
|
|
64
|
+
v = v * v * v;
|
|
65
|
+
const u = rng();
|
|
66
|
+
if (u < 1 - 0.0331 * x * x * x * x)
|
|
67
|
+
return d * v;
|
|
68
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
|
|
69
|
+
return d * v;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Beta(a, b) as X / (X + Y) with X ~ Gamma(a), Y ~ Gamma(b). */
|
|
73
|
+
export function beta(a, b, rng) {
|
|
74
|
+
const x = gamma(a, rng);
|
|
75
|
+
const y = gamma(b, rng);
|
|
76
|
+
return x / (x + y);
|
|
77
|
+
}
|
|
78
|
+
/** The 2.5th and 97.5th percentiles of a sample (nearest-rank, sorted in place). */
|
|
79
|
+
export function percentile95(values) {
|
|
80
|
+
const sorted = [...values].sort((p, q) => p - q);
|
|
81
|
+
const at = (q) => sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
|
|
82
|
+
return [at(0.025), at(0.975)];
|
|
83
|
+
}
|
|
84
|
+
/** Sensitivity (recall on the positive class) and specificity from a confusion matrix; null where the denominator is zero. */
|
|
85
|
+
export function sensitivity(c) {
|
|
86
|
+
return c.tp + c.fn === 0 ? null : c.tp / (c.tp + c.fn);
|
|
87
|
+
}
|
|
88
|
+
export function specificity(c) {
|
|
89
|
+
return c.tn + c.fp === 0 ? null : c.tn / (c.tn + c.fp);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Positive predictive value at prevalence π: of the outputs the rule fires
|
|
93
|
+
* on, the share that are real violations, when a share π of all outputs are
|
|
94
|
+
* violations. The published precision is the PPV at the corpus prevalence
|
|
95
|
+
* (about one half); at one percent prevalence the same rule's fire is worth
|
|
96
|
+
* far less, and this is the arithmetic that says how much.
|
|
97
|
+
*/
|
|
98
|
+
export function ppv(sens, spec, prevalence) {
|
|
99
|
+
const truePos = sens * prevalence;
|
|
100
|
+
const falsePos = (1 - spec) * (1 - prevalence);
|
|
101
|
+
return truePos + falsePos === 0 ? 0 : truePos / (truePos + falsePos);
|
|
102
|
+
}
|
|
103
|
+
/** P(violation | the rule did not fire) at prevalence π — the residual miss rate. */
|
|
104
|
+
export function missRate(sens, spec, prevalence) {
|
|
105
|
+
const missed = (1 - sens) * prevalence;
|
|
106
|
+
const trueNeg = spec * (1 - prevalence);
|
|
107
|
+
return missed + trueNeg === 0 ? 0 : missed / (missed + trueNeg);
|
|
108
|
+
}
|
|
109
|
+
export const round4 = (x) => Math.round(x * 10_000) / 10_000;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Coverage, EvalResult, EvalRule, EvalRuleResult, Need, Provenance, Verdict } from '../types/eval.js';
|
|
2
|
+
import type { EffectiveCriticality } from './criticality.js';
|
|
3
|
+
/**
|
|
4
|
+
* The basis of the verdict under the shipped composer (a weighted mean plus
|
|
5
|
+
* the critical veto). `passed` is `state === 'pass'` and equals the engine's
|
|
6
|
+
* own `passed` for every result; the basis says which layer decided.
|
|
7
|
+
*/
|
|
8
|
+
export declare function deriveVerdict(result: Pick<EvalResult, 'passed' | 'score' | 'rule_results' | 'insufficient_data' | 'critical_failures' | 'rules_evaluated'>, threshold: number): Verdict;
|
|
9
|
+
/**
|
|
10
|
+
* Which evaluation questions were judged, which were not and why. At write
|
|
11
|
+
* time the engine passes the inputs the call carried; at read time they are
|
|
12
|
+
* reconstructed as the union of what the rules saw (a rule that saw an input
|
|
13
|
+
* proves the call carried it; one that did not cannot prove the reverse).
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveCoverage(ruleResults: readonly EvalRuleResult[], present?: ReadonlySet<Need>): Coverage;
|
|
16
|
+
/** The critical rules that skipped — derived from the stamped flags on every read, never a column. */
|
|
17
|
+
export declare function deriveCriticalSkipped(ruleResults: readonly EvalRuleResult[]): string[] | undefined;
|
|
18
|
+
/** sha256 over the rules that ran — name, definition version, kind, effective criticality, weight — so two evaluations under the same ruleset hash the same. */
|
|
19
|
+
export declare function rulesetHash(rules: readonly EvalRule[], resolve: (rule: EvalRule) => EffectiveCriticality): string;
|
|
20
|
+
/** sha256 over the evaluation configuration that shapes a verdict. */
|
|
21
|
+
export declare function configHash(config: {
|
|
22
|
+
threshold: number;
|
|
23
|
+
ruleThresholds?: Record<string, unknown>;
|
|
24
|
+
criticalRules?: readonly string[];
|
|
25
|
+
nonCriticalRules?: readonly string[];
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function buildProvenance(input: {
|
|
28
|
+
irisVersion: string;
|
|
29
|
+
rulesetHash: string;
|
|
30
|
+
configHash: string;
|
|
31
|
+
threshold: number;
|
|
32
|
+
ruleThresholds?: Record<string, unknown>;
|
|
33
|
+
judgedAt: string;
|
|
34
|
+
}): Provenance;
|