@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,46 +1,7 @@
|
|
|
1
1
|
import { MAX_EVIDENCE_ITEMS } from '../../types/eval.js';
|
|
2
|
+
import { normalise, toRawSpan } from '../text/normalise.js';
|
|
3
|
+
import { luhn, iban, ssnStructure } from '../text/checksums.js';
|
|
2
4
|
import { acknowledgesFailure, failureReason, isFailedCall, skipWithoutTrajectory, truncate, } from './trajectory.js';
|
|
3
|
-
/*
|
|
4
|
-
* PII pattern library — expanded v0.3.1; credential class + placeholder
|
|
5
|
-
* suppression added after the gold-corpus measurement (fix/safety-rules-corpus).
|
|
6
|
-
*
|
|
7
|
-
* Each entry: human-readable name + regex + optional `placeholders` list.
|
|
8
|
-
* Order doesn't matter; all patterns evaluate. Word-boundary anchors avoid
|
|
9
|
-
* matching inside larger strings where appropriate.
|
|
10
|
-
*
|
|
11
|
-
* `placeholders` suppresses documentation values that are PII-shaped but by
|
|
12
|
-
* definition not PII: RFC 2606 example domains, the reserved 555 fictional
|
|
13
|
-
* phone block and toll-free lines, published payment test cards, masked
|
|
14
|
-
* keys, and 10-digit runs with no separators (Unix timestamps, JWTs and
|
|
15
|
-
* rate-limit headers read as "phone numbers"). The canonical documentation
|
|
16
|
-
* SSN is deliberately NOT suppressed — see the SSN entry below (#362).
|
|
17
|
-
* A pattern only fails the rule when at least one of its matches is NOT
|
|
18
|
-
* covered by a placeholder — so real PII beside a placeholder still fails.
|
|
19
|
-
*/
|
|
20
|
-
/*
|
|
21
|
-
* Every pattern here runs against ATTACKER-CONTROLLED text — agent output is
|
|
22
|
-
* untrusted by definition (resolve.ts states this outright), and any agent
|
|
23
|
-
* that summarises a web page, reads email, or handles user tickets can be
|
|
24
|
-
* fed a crafted string straight into evaluate_output.
|
|
25
|
-
*
|
|
26
|
-
* So: no ambiguous quantifiers. The rule that bit us was `\s*[:.]?\s*` in
|
|
27
|
-
* DOB and Medical Record Number — two adjacent unbounded whitespace
|
|
28
|
-
* quantifiers give the engine N+1 ways to split a run of N spaces, each of
|
|
29
|
-
* which fails at the trailing character class. Cost was quadratic in the
|
|
30
|
-
* input: 'MRN' + N spaces + '!' measured 31ms at 4k, 118ms at 8k, 468ms at
|
|
31
|
-
* 16k, and did not finish at the 1MB body limit. Node is single-threaded,
|
|
32
|
-
* so one call wedged the whole server.
|
|
33
|
-
*
|
|
34
|
-
* Bounded quantifiers ({0,8}) keep the alternatives constant regardless of
|
|
35
|
-
* input length. When adding a pattern, check for: adjacent quantifiers over
|
|
36
|
-
* overlapping character classes, nested quantifiers, and a character that
|
|
37
|
-
* can match both inside a + and as the following literal. Every pattern is
|
|
38
|
-
* asserted against the empirical backtracking probe (regex-budget.ts) in
|
|
39
|
-
* tests/unit/eval/safety-hardening.test.ts.
|
|
40
|
-
*
|
|
41
|
-
* Exported so the claims drift test can assert .claims.json counts against
|
|
42
|
-
* the runtime truth (tests/claims-eval-rules-counts.test.ts).
|
|
43
|
-
*/
|
|
44
5
|
export const PII_PATTERNS = [
|
|
45
6
|
// Original v0.3.0 patterns
|
|
46
7
|
/*
|
|
@@ -60,10 +21,11 @@ export const PII_PATTERNS = [
|
|
|
60
21
|
* that quotes the example costs a moment of noise, while a false negative
|
|
61
22
|
* on the canonical shape costs trust in every other result.
|
|
62
23
|
*/
|
|
63
|
-
{ name: 'SSN', pattern: /\b\d{3}-\d{2}-\d{4}\b
|
|
24
|
+
{ name: 'SSN', pattern: /\b\d{3}-\d{2}-\d{4}\b/, validate: ssnStructure },
|
|
64
25
|
{
|
|
65
26
|
name: 'Credit Card',
|
|
66
27
|
pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/,
|
|
28
|
+
validate: luhn,
|
|
67
29
|
// Published Stripe test cards — documentation values, never real PANs.
|
|
68
30
|
placeholders: [
|
|
69
31
|
/^4242[-\s]?4242[-\s]?4242[-\s]?4242$/,
|
|
@@ -106,7 +68,7 @@ export const PII_PATTERNS = [
|
|
|
106
68
|
},
|
|
107
69
|
// v0.3.1 additions
|
|
108
70
|
// IBAN: 2 letters + 2 digits + 1-30 alphanumeric (international bank account number)
|
|
109
|
-
{ name: 'IBAN', pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b
|
|
71
|
+
{ name: 'IBAN', pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b/, validate: iban },
|
|
110
72
|
/*
|
|
111
73
|
* US passport — CONTEXT-ANCHORED, like DOB and MRN below. A legacy
|
|
112
74
|
* passport number is nine bare digits and the modern (2021+) format is
|
|
@@ -200,13 +162,17 @@ export const PII_PATTERNS = [
|
|
|
200
162
|
* test pins it), so the boolean form stays and the span form below adds
|
|
201
163
|
* the evidence beside it.
|
|
202
164
|
*/
|
|
203
|
-
function piiPatternMatches(output, pattern, placeholders) {
|
|
204
|
-
if (!placeholders)
|
|
165
|
+
function piiPatternMatches(output, pattern, placeholders, validate) {
|
|
166
|
+
if (!placeholders && !validate)
|
|
205
167
|
return { fired: pattern.test(output), suppressed: 0 };
|
|
206
168
|
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
207
169
|
let suppressed = 0;
|
|
208
170
|
for (const match of output.matchAll(global)) {
|
|
209
|
-
|
|
171
|
+
// A structural failure is not a suppressed placeholder: the value is not
|
|
172
|
+
// documentation, it is simply not the thing the pattern is looking for.
|
|
173
|
+
if (validate && !validate(match[0]))
|
|
174
|
+
continue;
|
|
175
|
+
if (!placeholders?.some((placeholder) => placeholder.test(match[0])))
|
|
210
176
|
return { fired: true, suppressed };
|
|
211
177
|
suppressed++;
|
|
212
178
|
}
|
|
@@ -220,11 +186,13 @@ function piiPatternMatches(output, pattern, placeholders) {
|
|
|
220
186
|
* fires when this returns at least one span; that is the same condition the
|
|
221
187
|
* boolean form had, so no verdict moves.
|
|
222
188
|
*/
|
|
223
|
-
function piiPatternSpans(output, pattern, placeholders) {
|
|
189
|
+
function piiPatternSpans(output, pattern, placeholders, validate) {
|
|
224
190
|
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
225
191
|
const spans = [];
|
|
226
192
|
let suppressed = 0;
|
|
227
193
|
for (const match of output.matchAll(global)) {
|
|
194
|
+
if (validate && !validate(match[0]))
|
|
195
|
+
continue;
|
|
228
196
|
if (placeholders && placeholders.some((placeholder) => placeholder.test(match[0]))) {
|
|
229
197
|
suppressed++;
|
|
230
198
|
continue;
|
|
@@ -286,11 +254,22 @@ export const noPii = {
|
|
|
286
254
|
const found = [];
|
|
287
255
|
const evidence = [];
|
|
288
256
|
const suppressed = new Map();
|
|
289
|
-
|
|
290
|
-
|
|
257
|
+
/*
|
|
258
|
+
* Match the FOLDED text and report RAW spans (0.10.0). Before this, a
|
|
259
|
+
* full-width digit or a Cyrillic lookalike inside a card number defeated
|
|
260
|
+
* every pattern here: the transforms table measured 0% recall under
|
|
261
|
+
* full-width forms and 22% under homoglyphs. The offset map is what
|
|
262
|
+
* keeps arc 1's evidence contract — a span still indexes the output the
|
|
263
|
+
* caller sent, and it covers the obfuscating characters as part of the
|
|
264
|
+
* finding, which is what a redaction pass needs.
|
|
265
|
+
*/
|
|
266
|
+
const folded = normalise(context.output);
|
|
267
|
+
for (const { name, pattern, placeholders, validate } of PII_PATTERNS) {
|
|
268
|
+
const { fired, suppressed: ignored } = piiPatternMatches(folded.text, pattern, placeholders, validate);
|
|
291
269
|
if (fired) {
|
|
292
270
|
found.push(name);
|
|
293
|
-
for (const [
|
|
271
|
+
for (const [s, e] of piiPatternSpans(folded.text, pattern, placeholders, validate).spans) {
|
|
272
|
+
const [start, end] = toRawSpan(folded, s, e);
|
|
294
273
|
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
295
274
|
evidence.push({ type: 'span', source: 'output', start, end, label: name });
|
|
296
275
|
}
|
|
@@ -337,19 +316,27 @@ export const noBlocklistWords = {
|
|
|
337
316
|
critical: true,
|
|
338
317
|
evaluate(context) {
|
|
339
318
|
const blocklist = context.customConfig?.blocklist ?? DEFAULT_BLOCKLIST;
|
|
340
|
-
|
|
319
|
+
/*
|
|
320
|
+
* The folded text (0.10.0). This rule survived nothing but a change of
|
|
321
|
+
* case in the transforms table: a zero-width space, a homoglyph or a
|
|
322
|
+
* line break inside a banned phrase defeated it completely, which is a
|
|
323
|
+
* poor property for the one rule a deployment configures as a policy.
|
|
324
|
+
*/
|
|
325
|
+
const folded = normalise(context.output);
|
|
326
|
+
const lower = folded.text.toLowerCase();
|
|
341
327
|
const found = blocklist.filter((word) => lower.includes(word.toLowerCase()));
|
|
342
328
|
const passed = found.length === 0;
|
|
343
329
|
// Offsets are only meaningful when lowercasing preserved length (it does
|
|
344
330
|
// for ASCII; a few scripts expand). Otherwise the evidence names the
|
|
345
331
|
// phrase count without a span.
|
|
346
332
|
const evidence = [];
|
|
347
|
-
if (lower.length ===
|
|
333
|
+
if (lower.length === folded.text.length) {
|
|
348
334
|
for (const word of found) {
|
|
349
335
|
const needle = word.toLowerCase();
|
|
350
336
|
let at = lower.indexOf(needle);
|
|
351
337
|
while (at !== -1 && evidence.length < MAX_EVIDENCE_ITEMS) {
|
|
352
|
-
|
|
338
|
+
const [start, end] = toRawSpan(folded, at, at + needle.length);
|
|
339
|
+
evidence.push({ type: 'span', source: 'output', start, end, label: 'blocklist' });
|
|
353
340
|
at = lower.indexOf(needle, at + needle.length);
|
|
354
341
|
}
|
|
355
342
|
}
|
|
@@ -663,7 +650,16 @@ export const noInjectionPatterns = {
|
|
|
663
650
|
const found = [];
|
|
664
651
|
const evidence = [];
|
|
665
652
|
const raw = context.output;
|
|
666
|
-
|
|
653
|
+
/*
|
|
654
|
+
* Two layers (0.10.0): the shared fold every text rule uses, then the
|
|
655
|
+
* leetspeak substitution that belongs to this rule alone — it turns
|
|
656
|
+
* digits into letters, which is right for injection phrasing and would
|
|
657
|
+
* blind every digit-based detector if it were shared. Both layers
|
|
658
|
+
* preserve offsets into the folded text, so an obfuscated match can now
|
|
659
|
+
* be LOCATED in the raw output instead of merely named.
|
|
660
|
+
*/
|
|
661
|
+
const folded = normalise(raw);
|
|
662
|
+
const normalized = normalizeObfuscation(folded.text);
|
|
667
663
|
const rawSpans = quotedSpans(raw);
|
|
668
664
|
const normalizedSpans = normalized === raw ? rawSpans : quotedSpans(normalized);
|
|
669
665
|
for (let i = 0; i < INJECTION_PATTERNS.length; i++) {
|
|
@@ -679,11 +675,11 @@ export const noInjectionPatterns = {
|
|
|
679
675
|
}
|
|
680
676
|
else if (normalized !== raw && injectionPatternFires(normalized, normalizedSpans, pattern, respectQuotes)) {
|
|
681
677
|
found.push(`${pattern.source} (obfuscated)`);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
678
|
+
for (const [s, e] of injectionPatternSpans(normalized, normalizedSpans, pattern, respectQuotes)) {
|
|
679
|
+
const [start, end] = toRawSpan(folded, s, e);
|
|
680
|
+
if (evidence.length < MAX_EVIDENCE_ITEMS)
|
|
681
|
+
evidence.push({ type: 'span', source: 'output', start, end, label: `${label} (obfuscated)` });
|
|
682
|
+
}
|
|
687
683
|
}
|
|
688
684
|
}
|
|
689
685
|
const passed = found.length === 0;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Deterministic pseudo-randomness, for the credible interval on a verdict.
|
|
3
|
+
*
|
|
4
|
+
* The risk estimate draws each detector's sensitivity and specificity from
|
|
5
|
+
* a Beta posterior two thousand times to put an interval on p_bad. Those
|
|
6
|
+
* draws must be the same on every machine and every run, or the same
|
|
7
|
+
* evaluation would carry a different interval each time it was asked and
|
|
8
|
+
* the number would be unciteable. So: a fixed seed derived from the corpus
|
|
9
|
+
* version and the rules that spoke, and a generator that uses only integer
|
|
10
|
+
* operations and one divide.
|
|
11
|
+
*
|
|
12
|
+
* The proof harness has had the same two functions since the corpus
|
|
13
|
+
* shipped (`proof/lib/materialise.ts`), which is why they are written here
|
|
14
|
+
* rather than imported: the package ships `dist/` only, and a runtime read
|
|
15
|
+
* of anything under `proof/` finds nothing in an installed copy.
|
|
16
|
+
*/
|
|
17
|
+
/** FNV-1a, 32-bit: a string to a seed. */
|
|
18
|
+
export function fnv1a(s) {
|
|
19
|
+
let h = 0x811c9dc5;
|
|
20
|
+
for (let i = 0; i < s.length; i++) {
|
|
21
|
+
h ^= s.charCodeAt(i);
|
|
22
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
return h >>> 0;
|
|
25
|
+
}
|
|
26
|
+
/** mulberry32: small, fast, deterministic across engines (integer ops + one divide). */
|
|
27
|
+
export function mulberry32(seed) {
|
|
28
|
+
let a = seed >>> 0;
|
|
29
|
+
return () => {
|
|
30
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
31
|
+
let t = a;
|
|
32
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
33
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
34
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/eval/stamp.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export declare function skipClassOf(raw: EvalRuleResult): SkipClass | undefined;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function uncertaintyOf(rule: EvalRule, raw: EvalRuleResult): Uncertainty | undefined;
|
|
13
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'>;
|
|
14
|
+
export declare function stampRuleResult(rule: EvalRule, raw: EvalRuleResult, context: EvalContext, effective: EffectiveCriticality): Pick<EvalRuleResult, 'kind' | 'role' | 'question' | 'classes' | 'ruleVersion' | 'saw' | 'skipClass' | 'uncertainty' | 'origin'>;
|
package/dist/eval/stamp.js
CHANGED
|
@@ -81,6 +81,7 @@ export function stampRuleResult(rule, raw, context, effective) {
|
|
|
81
81
|
...(rule.question !== undefined ? { question: rule.question } : {}),
|
|
82
82
|
...(rule.classes !== undefined ? { classes: [...rule.classes] } : {}),
|
|
83
83
|
...(rule.version !== undefined ? { ruleVersion: rule.version } : {}),
|
|
84
|
+
...(rule.origin !== undefined ? { origin: rule.origin } : {}),
|
|
84
85
|
...(rule.needs !== undefined ? { saw: rule.needs.filter((n) => present.has(n)) } : {}),
|
|
85
86
|
...(skipClass !== undefined ? { skipClass } : {}),
|
|
86
87
|
...(uncertainty !== undefined ? { uncertainty } : {}),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Luhn check digit, as used by every major card network. Sum the digits
|
|
3
|
+
* right to left, doubling every second one and subtracting nine when the
|
|
4
|
+
* double exceeds nine; a valid number is divisible by ten.
|
|
5
|
+
*/
|
|
6
|
+
export declare function luhn(candidate: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* ISO 13616 mod-97: move the first four characters to the end, replace each
|
|
9
|
+
* letter with its position in the alphabet plus nine, and read the result as
|
|
10
|
+
* one large integer; a valid account gives a remainder of one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function iban(candidate: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The structural rules the Social Security Administration has never issued
|
|
15
|
+
* against: an area of 000, 666 or 900–999; a group of 00; a serial of 0000.
|
|
16
|
+
* This is not a checksum — the number carries none — but it rejects the
|
|
17
|
+
* digit runs that cannot be an SSN, which is the same job.
|
|
18
|
+
*
|
|
19
|
+
* The canonical fake 123-45-6789 is deliberately NOT rejected here: it is a
|
|
20
|
+
* real-shaped number, it is what people paste to test the detector, and the
|
|
21
|
+
* rule's own comment explains why letting it through is the honest choice.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ssnStructure(candidate: string): boolean;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Structural checks for the three PII patterns whose shape alone is not
|
|
3
|
+
* evidence of anything.
|
|
4
|
+
*
|
|
5
|
+
* A sixteen-digit run is not a card number, a two-letter-plus-digits token
|
|
6
|
+
* is not an international bank account, and three-two-four digits are not a
|
|
7
|
+
* social security number. Each of those formats carries a check that a real
|
|
8
|
+
* value satisfies and an arbitrary digit run almost never does, and applying
|
|
9
|
+
* it turns a shape match into a structure match.
|
|
10
|
+
*
|
|
11
|
+
* Two reasons this matters now rather than later. First, precision: the
|
|
12
|
+
* card pattern fires on an order id, a hash prefix or a timestamp run, and
|
|
13
|
+
* every such fire is a false positive a deployment has to explain away.
|
|
14
|
+
* Second, the normalisation pass (arc 3, A3-2a) folds full-width and
|
|
15
|
+
* circled digits into ASCII, so text that never looked like a card number
|
|
16
|
+
* can become one — `①②③④…` is a sixteen-digit run after NFKC. The fold is
|
|
17
|
+
* what makes evasion detectable and the checksum is what stops the fold
|
|
18
|
+
* from manufacturing findings. They ship together on purpose.
|
|
19
|
+
*
|
|
20
|
+
* Every function here is total and side-effect free: given a string it
|
|
21
|
+
* returns a boolean, and a value it cannot parse is not valid.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The Luhn check digit, as used by every major card network. Sum the digits
|
|
25
|
+
* right to left, doubling every second one and subtracting nine when the
|
|
26
|
+
* double exceeds nine; a valid number is divisible by ten.
|
|
27
|
+
*/
|
|
28
|
+
export function luhn(candidate) {
|
|
29
|
+
let sum = 0;
|
|
30
|
+
let double = false;
|
|
31
|
+
let digits = 0;
|
|
32
|
+
for (let i = candidate.length - 1; i >= 0; i--) {
|
|
33
|
+
const code = candidate.charCodeAt(i);
|
|
34
|
+
if (code < 48 || code > 57) {
|
|
35
|
+
// Separators a card number legitimately carries; anything else means
|
|
36
|
+
// this was never a card number.
|
|
37
|
+
if (candidate[i] === '-' || candidate[i] === ' ')
|
|
38
|
+
continue;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
let d = code - 48;
|
|
42
|
+
digits++;
|
|
43
|
+
if (double) {
|
|
44
|
+
d *= 2;
|
|
45
|
+
if (d > 9)
|
|
46
|
+
d -= 9;
|
|
47
|
+
}
|
|
48
|
+
sum += d;
|
|
49
|
+
double = !double;
|
|
50
|
+
}
|
|
51
|
+
if (digits < 13 || digits > 19)
|
|
52
|
+
return false;
|
|
53
|
+
return sum % 10 === 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ISO 13616 mod-97: move the first four characters to the end, replace each
|
|
57
|
+
* letter with its position in the alphabet plus nine, and read the result as
|
|
58
|
+
* one large integer; a valid account gives a remainder of one.
|
|
59
|
+
*/
|
|
60
|
+
export function iban(candidate) {
|
|
61
|
+
const s = candidate.replace(/[\s-]/g, '').toUpperCase();
|
|
62
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(s))
|
|
63
|
+
return false;
|
|
64
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
65
|
+
let remainder = 0;
|
|
66
|
+
for (const ch of rearranged) {
|
|
67
|
+
const code = ch.charCodeAt(0);
|
|
68
|
+
const part = code >= 65 && code <= 90 ? String(code - 55) : ch;
|
|
69
|
+
for (const digit of part) {
|
|
70
|
+
remainder = (remainder * 10 + (digit.charCodeAt(0) - 48)) % 97;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return remainder === 1;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The structural rules the Social Security Administration has never issued
|
|
77
|
+
* against: an area of 000, 666 or 900–999; a group of 00; a serial of 0000.
|
|
78
|
+
* This is not a checksum — the number carries none — but it rejects the
|
|
79
|
+
* digit runs that cannot be an SSN, which is the same job.
|
|
80
|
+
*
|
|
81
|
+
* The canonical fake 123-45-6789 is deliberately NOT rejected here: it is a
|
|
82
|
+
* real-shaped number, it is what people paste to test the detector, and the
|
|
83
|
+
* rule's own comment explains why letting it through is the honest choice.
|
|
84
|
+
*/
|
|
85
|
+
export function ssnStructure(candidate) {
|
|
86
|
+
const m = /^(\d{3})-(\d{2})-(\d{4})$/.exec(candidate.trim());
|
|
87
|
+
if (!m)
|
|
88
|
+
return false;
|
|
89
|
+
const [, area, group, serial] = m;
|
|
90
|
+
if (area === '000' || area === '666' || area[0] === '9')
|
|
91
|
+
return false;
|
|
92
|
+
if (group === '00')
|
|
93
|
+
return false;
|
|
94
|
+
if (serial === '0000')
|
|
95
|
+
return false;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface Normalised {
|
|
2
|
+
/** The folded text every pattern should match against. */
|
|
3
|
+
text: string;
|
|
4
|
+
/**
|
|
5
|
+
* `map[i]` is the offset in the RAW string that normalised character `i`
|
|
6
|
+
* came from. Length is `text.length + 1`; the final entry is the raw
|
|
7
|
+
* length, so a normalised span `[s, e)` becomes the raw span
|
|
8
|
+
* `[map[s], map[e])` with no special case at the end of the string.
|
|
9
|
+
*
|
|
10
|
+
* Built on first read. A rule asks for it only when a pattern actually
|
|
11
|
+
* fires, and on a one-megabyte output the array is four megabytes — so
|
|
12
|
+
* the overwhelming majority of evaluations, which find nothing, never
|
|
13
|
+
* allocate it.
|
|
14
|
+
*/
|
|
15
|
+
readonly map: Int32Array;
|
|
16
|
+
/**
|
|
17
|
+
* True when the fold changed nothing AND the map is the identity, so a
|
|
18
|
+
* caller can use normalised offsets as raw offsets directly.
|
|
19
|
+
*/
|
|
20
|
+
unchanged: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Folds `raw` for matching and returns the offset map that puts evidence back on the raw text. */
|
|
23
|
+
export declare function normalise(raw: string): Normalised;
|
|
24
|
+
/**
|
|
25
|
+
* A span in normalised coordinates as a span in raw coordinates. Always
|
|
26
|
+
* widens rather than narrows: when characters were dropped between the last
|
|
27
|
+
* matched character and the next kept one, the raw span covers them, which
|
|
28
|
+
* is what a reader wants — the evasion is part of the evidence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toRawSpan(n: Normalised, start: number, end: number): [number, number];
|