@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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/config/defaults.js +15 -0
  3. package/dist/dashboard/assets/{index-Cz8_oOqG.js → index-CeJbaq6m.js} +1 -1
  4. package/dist/dashboard/index.html +1 -1
  5. package/dist/dashboard/routes/traces.js +2 -2
  6. package/dist/dashboard/seed-demo-data.js +1 -1
  7. package/dist/eval/citation-verify/verifier.d.ts +16 -1
  8. package/dist/eval/citation-verify/verifier.js +14 -4
  9. package/dist/eval/compose.d.ts +57 -0
  10. package/dist/eval/compose.js +179 -0
  11. package/dist/eval/criticality.d.ts +7 -0
  12. package/dist/eval/decision-moment.js +33 -4
  13. package/dist/eval/engine.d.ts +5 -2
  14. package/dist/eval/engine.js +81 -13
  15. package/dist/eval/llm-judge/evaluator.d.ts +20 -0
  16. package/dist/eval/llm-judge/evaluator.js +10 -1
  17. package/dist/eval/published-accuracy.d.ts +22 -22
  18. package/dist/eval/published-accuracy.js +11 -11
  19. package/dist/eval/risk.d.ts +60 -0
  20. package/dist/eval/risk.js +187 -0
  21. package/dist/eval/rules/completeness.js +5 -1
  22. package/dist/eval/rules/cost.d.ts +1 -1
  23. package/dist/eval/rules/cost.js +6 -6
  24. package/dist/eval/rules/custom.js +1 -0
  25. package/dist/eval/rules/relevance.js +7 -2
  26. package/dist/eval/rules/safety.d.ts +6 -2
  27. package/dist/eval/rules/safety.js +55 -59
  28. package/dist/eval/seeded-random.d.ts +4 -0
  29. package/dist/eval/seeded-random.js +36 -0
  30. package/dist/eval/stamp.d.ts +1 -1
  31. package/dist/eval/stamp.js +1 -0
  32. package/dist/eval/text/checksums.d.ts +23 -0
  33. package/dist/eval/text/checksums.js +97 -0
  34. package/dist/eval/text/normalise.d.ts +30 -0
  35. package/dist/eval/text/normalise.js +265 -0
  36. package/dist/eval/text/sentences.d.ts +15 -0
  37. package/dist/eval/text/sentences.js +149 -0
  38. package/dist/self-test.js +3 -3
  39. package/dist/storage/sqlite-adapter.js +16 -2
  40. package/dist/tools/evaluate-output.js +2 -2
  41. package/dist/tools/evaluate-with-llm-judge.d.ts +3 -0
  42. package/dist/tools/evaluate-with-llm-judge.js +29 -1
  43. package/dist/tools/verify-citations.d.ts +2 -1
  44. package/dist/tools/verify-citations.js +25 -4
  45. package/dist/types/config.d.ts +35 -0
  46. package/dist/types/eval.d.ts +51 -0
  47. package/package.json +1 -1
  48. 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
- if (!placeholders.some((placeholder) => placeholder.test(match[0])))
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
- for (const { name, pattern, placeholders } of PII_PATTERNS) {
290
- const { fired, suppressed: ignored } = piiPatternMatches(context.output, pattern, placeholders);
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 [start, end] of piiPatternSpans(context.output, pattern, placeholders).spans) {
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
- const lower = context.output.toLowerCase();
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 === context.output.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
- evidence.push({ type: 'span', source: 'output', start: at, end: at + needle.length, label: 'blocklist' });
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
- const normalized = normalizeObfuscation(raw);
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
- // 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 });
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,4 @@
1
+ /** FNV-1a, 32-bit: a string to a seed. */
2
+ export declare function fnv1a(s: string): number;
3
+ /** mulberry32: small, fast, deterministic across engines (integer ops + one divide). */
4
+ export declare function mulberry32(seed: number): () => number;
@@ -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
+ }
@@ -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'>;
@@ -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];