@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.
Files changed (131) hide show
  1. package/README.md +9 -2
  2. package/dist/capabilities.d.ts +64 -0
  3. package/dist/capabilities.js +65 -0
  4. package/dist/config/defaults.js +17 -0
  5. package/dist/custom-rule-store.d.ts +4 -0
  6. package/dist/custom-rule-store.js +8 -3
  7. package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
  8. package/dist/dashboard/index.html +1 -1
  9. package/dist/dashboard/routes/capabilities.d.ts +3 -0
  10. package/dist/dashboard/routes/capabilities.js +11 -0
  11. package/dist/dashboard/routes/health.d.ts +5 -1
  12. package/dist/dashboard/routes/health.js +15 -3
  13. package/dist/dashboard/routes/rules.js +4 -1
  14. package/dist/dashboard/routes/traces.d.ts +3 -0
  15. package/dist/dashboard/routes/traces.js +11 -30
  16. package/dist/dashboard/seed-demo-data.js +1 -1
  17. package/dist/dashboard/server.d.ts +2 -0
  18. package/dist/dashboard/server.js +6 -2
  19. package/dist/eval/accuracy.d.ts +41 -0
  20. package/dist/eval/accuracy.js +97 -0
  21. package/dist/eval/citation-verify/verifier.d.ts +16 -1
  22. package/dist/eval/citation-verify/verifier.js +14 -4
  23. package/dist/eval/compose.d.ts +57 -0
  24. package/dist/eval/compose.js +179 -0
  25. package/dist/eval/criticality.d.ts +15 -1
  26. package/dist/eval/criticality.js +6 -0
  27. package/dist/eval/decision-moment.js +33 -4
  28. package/dist/eval/dormant.d.ts +4 -0
  29. package/dist/eval/dormant.js +22 -0
  30. package/dist/eval/engine.d.ts +6 -2
  31. package/dist/eval/engine.js +126 -12
  32. package/dist/eval/failure-classes.d.ts +8 -0
  33. package/dist/eval/failure-classes.js +18 -0
  34. package/dist/eval/llm-judge/evaluator.d.ts +30 -0
  35. package/dist/eval/llm-judge/evaluator.js +26 -2
  36. package/dist/eval/published-accuracy.d.ts +230 -0
  37. package/dist/eval/published-accuracy.js +86 -0
  38. package/dist/eval/questions.d.ts +12 -0
  39. package/dist/eval/questions.js +14 -0
  40. package/dist/eval/response-schema.d.ts +652 -0
  41. package/dist/eval/response-schema.js +130 -0
  42. package/dist/eval/response.d.ts +12 -0
  43. package/dist/eval/response.js +30 -0
  44. package/dist/eval/risk.d.ts +60 -0
  45. package/dist/eval/risk.js +187 -0
  46. package/dist/eval/rules/completeness.js +36 -1
  47. package/dist/eval/rules/cost.d.ts +2 -2
  48. package/dist/eval/rules/cost.js +50 -6
  49. package/dist/eval/rules/custom.d.ts +0 -12
  50. package/dist/eval/rules/custom.js +22 -0
  51. package/dist/eval/rules/relevance.js +23 -2
  52. package/dist/eval/rules/safety.d.ts +6 -2
  53. package/dist/eval/rules/safety.js +224 -51
  54. package/dist/eval/seeded-random.d.ts +4 -0
  55. package/dist/eval/seeded-random.js +36 -0
  56. package/dist/eval/stamp.d.ts +14 -0
  57. package/dist/eval/stamp.js +89 -0
  58. package/dist/eval/stats.d.ts +33 -0
  59. package/dist/eval/stats.js +109 -0
  60. package/dist/eval/text/checksums.d.ts +23 -0
  61. package/dist/eval/text/checksums.js +97 -0
  62. package/dist/eval/text/normalise.d.ts +30 -0
  63. package/dist/eval/text/normalise.js +265 -0
  64. package/dist/eval/text/sentences.d.ts +15 -0
  65. package/dist/eval/text/sentences.js +149 -0
  66. package/dist/eval/verdict.d.ts +34 -0
  67. package/dist/eval/verdict.js +131 -0
  68. package/dist/index.js +5 -28
  69. package/dist/instructions.d.ts +17 -0
  70. package/dist/instructions.js +53 -0
  71. package/dist/judge-enablement.d.ts +34 -0
  72. package/dist/judge-enablement.js +78 -0
  73. package/dist/judge-enablement.json +10 -0
  74. package/dist/preferences.d.ts +1 -1
  75. package/dist/prompts.d.ts +3 -0
  76. package/dist/prompts.js +29 -0
  77. package/dist/resources/index.d.ts +5 -2
  78. package/dist/resources/index.js +65 -5
  79. package/dist/resources/uris.d.ts +12 -0
  80. package/dist/resources/uris.js +24 -0
  81. package/dist/retention.d.ts +20 -0
  82. package/dist/retention.js +44 -0
  83. package/dist/self-test.d.ts +1 -0
  84. package/dist/self-test.js +17 -3
  85. package/dist/server.d.ts +10 -1
  86. package/dist/server.js +34 -7
  87. package/dist/storage/index.js +1 -1
  88. package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
  89. package/dist/storage/migrations/007-eval-provenance.js +30 -0
  90. package/dist/storage/migrations/index.js +24 -4
  91. package/dist/storage/sqlite-adapter.d.ts +26 -1
  92. package/dist/storage/sqlite-adapter.js +149 -15
  93. package/dist/tools/delete-rule.d.ts +8 -0
  94. package/dist/tools/delete-rule.js +30 -38
  95. package/dist/tools/delete-trace.d.ts +5 -0
  96. package/dist/tools/delete-trace.js +24 -27
  97. package/dist/tools/deploy-rule.d.ts +13 -1
  98. package/dist/tools/deploy-rule.js +37 -34
  99. package/dist/tools/describe.d.ts +20 -0
  100. package/dist/tools/describe.js +36 -0
  101. package/dist/tools/errors.d.ts +36 -0
  102. package/dist/tools/errors.js +134 -0
  103. package/dist/tools/evaluate-output.d.ts +8 -1
  104. package/dist/tools/evaluate-output.js +39 -60
  105. package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
  106. package/dist/tools/evaluate-with-llm-judge.js +124 -69
  107. package/dist/tools/get-traces.d.ts +9 -0
  108. package/dist/tools/get-traces.js +29 -28
  109. package/dist/tools/index.d.ts +8 -0
  110. package/dist/tools/index.js +22 -1
  111. package/dist/tools/list-rules.d.ts +13 -0
  112. package/dist/tools/list-rules.js +43 -46
  113. package/dist/tools/log-trace.d.ts +4 -0
  114. package/dist/tools/log-trace.js +31 -29
  115. package/dist/tools/respond.d.ts +42 -0
  116. package/dist/tools/respond.js +90 -0
  117. package/dist/tools/strict-input.js +1 -1
  118. package/dist/tools/trace-link.d.ts +2 -0
  119. package/dist/tools/trace-link.js +13 -2
  120. package/dist/tools/verify-citations.d.ts +18 -2
  121. package/dist/tools/verify-citations.js +122 -96
  122. package/dist/types/config.d.ts +44 -0
  123. package/dist/types/eval.d.ts +309 -0
  124. package/dist/types/eval.js +2 -1
  125. package/dist/types/query.d.ts +2 -0
  126. package/package.json +1 -1
  127. package/server.json +2 -2
  128. package/dist/resources/dashboard-summary.d.ts +0 -3
  129. package/dist/resources/dashboard-summary.js +0 -16
  130. package/dist/resources/trace-detail.d.ts +0 -3
  131. package/dist/resources/trace-detail.js +0 -30
@@ -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
+ }
@@ -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' | 'origin'>;
@@ -0,0 +1,89 @@
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.origin !== undefined ? { origin: rule.origin } : {}),
85
+ ...(rule.needs !== undefined ? { saw: rule.needs.filter((n) => present.has(n)) } : {}),
86
+ ...(skipClass !== undefined ? { skipClass } : {}),
87
+ ...(uncertainty !== undefined ? { uncertainty } : {}),
88
+ };
89
+ }
@@ -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,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];