euredact 0.3.8 → 0.4.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.
@@ -0,0 +1,215 @@
1
+ /**
2
+ * [CLOUD EXTENSION] The cloud client.
3
+ *
4
+ * Speaks to the euRedact inference service: a deterministic rules engine
5
+ * followed by a fine-tuned model asked only *what did the rules miss?* The
6
+ * service returns both the redacted document and located spans, so this
7
+ * module's job is transport and translation, not detection.
8
+ *
9
+ * Three things it hides from the caller:
10
+ *
11
+ * - **Whether the document finished inside the service's sync window.** A short
12
+ * document comes back on the original request; a long one, or one arriving
13
+ * while a GPU node is cold, returns a job handle and is polled. Callers
14
+ * should never write that branch, so `redact()` resolves either way.
15
+ * - **Retries.** Every request carries an `Idempotency-Key`, so a retry after a
16
+ * timeout cannot produce a second job or a second usage row.
17
+ * - **`Retry-After`.** When the service says how long to wait, that is obeyed
18
+ * rather than second-guessed with a backoff curve.
19
+ *
20
+ * Zero dependencies, like the rest of the package: this uses the platform's
21
+ * own `fetch`.
22
+ */
23
+ import { getConfig, } from "./config.js";
24
+ import { CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, } from "./errors.js";
25
+ import { canonicalType, DetectionSource, EntityType, } from "../types.js";
26
+ const RETRY_STATUS = new Set([429, 500, 502, 503, 504]);
27
+ const MAX_BACKOFF_MS = 30_000;
28
+ function requireFetch(supplied) {
29
+ const impl = supplied ?? globalThis.fetch;
30
+ if (typeof impl !== "function") {
31
+ throw new NotConfiguredError("the cloud tier needs a global fetch. Node 18+ provides one; on Node 16 " +
32
+ "supply one via the fetchImpl option or install a polyfill.");
33
+ }
34
+ return impl;
35
+ }
36
+ function sleep(ms) {
37
+ return new Promise(resolve => setTimeout(resolve, ms));
38
+ }
39
+ function retryAfterMs(headers) {
40
+ const raw = headers?.get("Retry-After");
41
+ if (!raw)
42
+ return null;
43
+ const seconds = Number(raw);
44
+ // The HTTP-date form is legal too; fall back to backoff rather than parse it.
45
+ return Number.isFinite(seconds) ? Math.max(0, seconds * 1000) : null;
46
+ }
47
+ /**
48
+ * Full jitter. A synchronised retry storm from many clients is how a
49
+ * recovering service is knocked back over.
50
+ */
51
+ function backoffMs(attempt, retryAfter) {
52
+ if (retryAfter !== null)
53
+ return Math.min(retryAfter, MAX_BACKOFF_MS);
54
+ return Math.min(MAX_BACKOFF_MS, Math.random() * 2 ** attempt * 1000);
55
+ }
56
+ function uuid() {
57
+ const c = globalThis.crypto;
58
+ if (c && typeof c.randomUUID === "function")
59
+ return c.randomUUID();
60
+ return `idem-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
61
+ }
62
+ /**
63
+ * Canon name -> EntityType, tolerating one this build has not heard of.
64
+ *
65
+ * An unknown type is passed through as a plain string rather than dropped:
66
+ * `Detection.entityType` is `EntityType | string` precisely so a client one
67
+ * release behind the service does not silently lose a whole category of PII.
68
+ */
69
+ function toEntityType(raw) {
70
+ const canonical = canonicalType(raw);
71
+ const known = Object.values(EntityType).includes(canonical);
72
+ return known ? canonical : canonical;
73
+ }
74
+ function toResult(payload, text) {
75
+ const detections = (payload.entities ?? []).map(span => ({
76
+ entityType: toEntityType(span.type),
77
+ start: span.start,
78
+ end: span.end,
79
+ text: span.text,
80
+ source: span.source === "model" ? DetectionSource.CLOUD : DetectionSource.RULES,
81
+ country: null,
82
+ confidence: span.confidence ?? "high",
83
+ }));
84
+ detections.sort((a, b) => a.start - b.start || b.end - a.end);
85
+ return {
86
+ redactedText: payload.redacted_text ?? text,
87
+ detections,
88
+ source: "cloud",
89
+ degraded: false,
90
+ inferredCountries: [],
91
+ evidence: [],
92
+ detectionMode: "declared",
93
+ };
94
+ }
95
+ export class CloudClient {
96
+ config;
97
+ constructor(config) {
98
+ const resolved = config ?? getConfig();
99
+ if (resolved === null)
100
+ throw new NotConfiguredError();
101
+ this.config = resolved;
102
+ }
103
+ headers(idempotencyKey) {
104
+ return {
105
+ Authorization: `Bearer ${this.config.apiKey}`,
106
+ "Content-Type": "application/json",
107
+ // A retry after a timeout must not create a second job or a second usage
108
+ // row. The key is per logical document, not per attempt.
109
+ "Idempotency-Key": idempotencyKey,
110
+ ...this.config.headers,
111
+ };
112
+ }
113
+ raiseFor(status, payload) {
114
+ const message = payload.error ?? `HTTP ${status}`;
115
+ const detail = payload.detail ?? {};
116
+ if (status === 401)
117
+ throw new CloudError(`authentication failed: ${message}`, status);
118
+ if (status === 413)
119
+ throw new TooLargeError(message, status, detail);
120
+ if (status === 429)
121
+ throw new QuotaExceededError(message, status, detail);
122
+ throw new CloudError(message, status, detail);
123
+ }
124
+ async redact(text, options) {
125
+ const doFetch = requireFetch(options.fetchImpl);
126
+ const key = options.idempotencyKey ?? uuid();
127
+ const body = JSON.stringify({
128
+ text,
129
+ country: options.country.toUpperCase(),
130
+ language: options.language ?? "",
131
+ priority: options.priority ?? "interactive",
132
+ rules_only: options.rulesOnly ?? false,
133
+ });
134
+ let attempt = 0;
135
+ for (;;) {
136
+ let response;
137
+ let payload = {};
138
+ try {
139
+ response = await this.withTimeout(doFetch, `${this.config.baseUrl}/v1/redact`, {
140
+ method: "POST",
141
+ headers: this.headers(key),
142
+ body,
143
+ });
144
+ payload = await readJson(response);
145
+ }
146
+ catch (e) {
147
+ if (attempt >= this.config.maxRetries) {
148
+ throw new CloudError(`cloud request failed: ${e instanceof Error ? e.message : String(e)}`);
149
+ }
150
+ }
151
+ if (response) {
152
+ if (response.status === 202) {
153
+ // Outlived the sync window. Location is where the answer will appear;
154
+ // polling it is this client's job, not the caller's.
155
+ const location = response.headers.get("Location") ??
156
+ payload.location ??
157
+ `/v1/jobs/${payload.job_id ?? ""}`;
158
+ return toResult(await this.poll(doFetch, location, key), text);
159
+ }
160
+ if (response.status === 200)
161
+ return toResult(payload, text);
162
+ if (!RETRY_STATUS.has(response.status) || attempt >= this.config.maxRetries) {
163
+ this.raiseFor(response.status, payload);
164
+ }
165
+ }
166
+ await sleep(backoffMs(attempt, retryAfterMs(response?.headers)));
167
+ attempt++;
168
+ }
169
+ }
170
+ async poll(doFetch, location, key) {
171
+ const url = location.startsWith("http")
172
+ ? location
173
+ : `${this.config.baseUrl}${location}`;
174
+ const deadline = Date.now() + this.config.pollTimeoutMs;
175
+ let delay = 500;
176
+ for (;;) {
177
+ const response = await this.withTimeout(doFetch, url, {
178
+ method: "GET",
179
+ headers: this.headers(key),
180
+ });
181
+ const payload = await readJson(response);
182
+ if (response.status === 200 && payload.redacted_text !== undefined) {
183
+ return payload;
184
+ }
185
+ if (response.status !== 200 && !RETRY_STATUS.has(response.status)) {
186
+ this.raiseFor(response.status, payload);
187
+ }
188
+ if (Date.now() >= deadline) {
189
+ throw new CloudError(`document did not complete within ${Math.round(this.config.pollTimeoutMs / 1000)}s (${url})`);
190
+ }
191
+ const after = retryAfterMs(response.headers);
192
+ await sleep(after ?? delay);
193
+ delay = Math.min(delay * 1.5, 5000);
194
+ }
195
+ }
196
+ async withTimeout(doFetch, url, init) {
197
+ const controller = new AbortController();
198
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
199
+ try {
200
+ return await doFetch(url, { ...init, signal: controller.signal });
201
+ }
202
+ finally {
203
+ clearTimeout(timer);
204
+ }
205
+ }
206
+ }
207
+ async function readJson(response) {
208
+ try {
209
+ const data = await response.json();
210
+ return data && typeof data === "object" ? data : {};
211
+ }
212
+ catch {
213
+ return {};
214
+ }
215
+ }
@@ -0,0 +1,43 @@
1
+ /** [CLOUD EXTENSION] Cloud tier configuration. */
2
+ export declare const DEFAULT_BASE_URL = "https://api.euredact.dev";
3
+ /**
4
+ * How long to wait for a single document. The service holds a connection open
5
+ * for its sync window and then hands back a job handle, so this has to exceed
6
+ * that window or the client abandons requests the service is about to answer.
7
+ */
8
+ export declare const DEFAULT_TIMEOUT_MS = 90000;
9
+ /**
10
+ * Total wall-clock budget for a document that outlives the sync window and has
11
+ * to be polled. Cold-starting a GPU node and loading an ~18 GB checkpoint takes
12
+ * minutes, and an async job absorbs that invisibly — but not forever.
13
+ */
14
+ export declare const DEFAULT_POLL_TIMEOUT_MS = 900000;
15
+ export declare const DEFAULT_MAX_RETRIES = 3;
16
+ export interface CloudConfig {
17
+ apiKey: string;
18
+ baseUrl: string;
19
+ timeoutMs: number;
20
+ pollTimeoutMs: number;
21
+ maxRetries: number;
22
+ /** Extra headers, for a proxy or a self-hosted deployment. */
23
+ headers: Record<string, string>;
24
+ }
25
+ export interface ConfigureOptions {
26
+ apiKey?: string;
27
+ baseUrl?: string;
28
+ timeoutMs?: number;
29
+ pollTimeoutMs?: number;
30
+ maxRetries?: number;
31
+ headers?: Record<string, string>;
32
+ }
33
+ /**
34
+ * Configure the cloud tier.
35
+ *
36
+ * Falls back to `EUREDACT_API_KEY` and `EUREDACT_BASE_URL` so a key never has
37
+ * to be written into source to get started.
38
+ */
39
+ export declare function configure(options?: ConfigureOptions): CloudConfig;
40
+ /** The active configuration, or null. Never throws. */
41
+ export declare function getConfig(): CloudConfig | null;
42
+ /** Forget the configuration. Mainly for tests. */
43
+ export declare function reset(): void;
@@ -0,0 +1,51 @@
1
+ /** [CLOUD EXTENSION] Cloud tier configuration. */
2
+ export const DEFAULT_BASE_URL = "https://api.euredact.dev";
3
+ /**
4
+ * How long to wait for a single document. The service holds a connection open
5
+ * for its sync window and then hands back a job handle, so this has to exceed
6
+ * that window or the client abandons requests the service is about to answer.
7
+ */
8
+ export const DEFAULT_TIMEOUT_MS = 90_000;
9
+ /**
10
+ * Total wall-clock budget for a document that outlives the sync window and has
11
+ * to be polled. Cold-starting a GPU node and loading an ~18 GB checkpoint takes
12
+ * minutes, and an async job absorbs that invisibly — but not forever.
13
+ */
14
+ export const DEFAULT_POLL_TIMEOUT_MS = 900_000;
15
+ export const DEFAULT_MAX_RETRIES = 3;
16
+ let config = null;
17
+ /** Read an env var where there is an environment to read. Browsers have none. */
18
+ function env(name) {
19
+ const proc = globalThis.process;
20
+ return proc?.env?.[name];
21
+ }
22
+ /**
23
+ * Configure the cloud tier.
24
+ *
25
+ * Falls back to `EUREDACT_API_KEY` and `EUREDACT_BASE_URL` so a key never has
26
+ * to be written into source to get started.
27
+ */
28
+ export function configure(options = {}) {
29
+ const apiKey = options.apiKey ?? env("EUREDACT_API_KEY") ?? "";
30
+ if (!apiKey) {
31
+ throw new Error("no API key: pass configure({ apiKey }) or set EUREDACT_API_KEY");
32
+ }
33
+ config = {
34
+ apiKey,
35
+ baseUrl: (options.baseUrl ?? env("EUREDACT_BASE_URL") ?? DEFAULT_BASE_URL)
36
+ .replace(/\/+$/, ""),
37
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
38
+ pollTimeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
39
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
40
+ headers: { ...(options.headers ?? {}) },
41
+ };
42
+ return config;
43
+ }
44
+ /** The active configuration, or null. Never throws. */
45
+ export function getConfig() {
46
+ return config;
47
+ }
48
+ /** Forget the configuration. Mainly for tests. */
49
+ export function reset() {
50
+ config = null;
51
+ }
@@ -0,0 +1,32 @@
1
+ /** [CLOUD EXTENSION] Errors the cloud tier can raise. */
2
+ export declare class CloudError extends Error {
3
+ readonly status?: number;
4
+ readonly detail: Record<string, unknown>;
5
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
6
+ }
7
+ /**
8
+ * Raised when the cloud tier is used without configuration.
9
+ *
10
+ * Deliberately an error rather than a quiet fallback to rules-only output.
11
+ * `mode: "cloud"` that silently returns rules-only results is the worst failure
12
+ * this library can have: the caller believes names, employers and diagnoses
13
+ * were checked, sees a plausible redacted document, and ships it with the PII
14
+ * still in it.
15
+ */
16
+ export declare class NotConfiguredError extends CloudError {
17
+ constructor(message?: string);
18
+ }
19
+ /** 429 after retries were exhausted. */
20
+ export declare class QuotaExceededError extends CloudError {
21
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
22
+ }
23
+ /**
24
+ * 413. The document is over the service's input cap.
25
+ *
26
+ * Permanent and not retryable. The model has never seen a chunk boundary, so
27
+ * the service refuses oversized input rather than splitting it and changing the
28
+ * accuracy story silently.
29
+ */
30
+ export declare class TooLargeError extends CloudError {
31
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
32
+ }
@@ -0,0 +1,48 @@
1
+ /** [CLOUD EXTENSION] Errors the cloud tier can raise. */
2
+ export class CloudError extends Error {
3
+ status;
4
+ detail;
5
+ constructor(message, status, detail) {
6
+ super(message);
7
+ this.name = "CloudError";
8
+ this.status = status;
9
+ this.detail = detail ?? {};
10
+ }
11
+ }
12
+ /**
13
+ * Raised when the cloud tier is used without configuration.
14
+ *
15
+ * Deliberately an error rather than a quiet fallback to rules-only output.
16
+ * `mode: "cloud"` that silently returns rules-only results is the worst failure
17
+ * this library can have: the caller believes names, employers and diagnoses
18
+ * were checked, sees a plausible redacted document, and ships it with the PII
19
+ * still in it.
20
+ */
21
+ export class NotConfiguredError extends CloudError {
22
+ constructor(message) {
23
+ super(message ??
24
+ "Cloud tier not configured. Call euredact.configure({ apiKey }) first, " +
25
+ "or set EUREDACT_API_KEY.");
26
+ this.name = "NotConfiguredError";
27
+ }
28
+ }
29
+ /** 429 after retries were exhausted. */
30
+ export class QuotaExceededError extends CloudError {
31
+ constructor(message, status, detail) {
32
+ super(message, status, detail);
33
+ this.name = "QuotaExceededError";
34
+ }
35
+ }
36
+ /**
37
+ * 413. The document is over the service's input cap.
38
+ *
39
+ * Permanent and not retryable. The model has never seen a chunk boundary, so
40
+ * the service refuses oversized input rather than splitting it and changing the
41
+ * accuracy story silently.
42
+ */
43
+ export class TooLargeError extends CloudError {
44
+ constructor(message, status, detail) {
45
+ super(message, status, detail);
46
+ this.name = "TooLargeError";
47
+ }
48
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [CLOUD EXTENSION] Cloud tier — rules engine plus a fine-tuned model.
3
+ *
4
+ * The rule engine catches what has a shape: IBANs, national IDs, phone
5
+ * numbers, anything with a checksum. It cannot catch what does not — a
6
+ * person's name, an employer, a diagnosis, a job title. Those are what the
7
+ * cloud tier adds, and the `EntityType` members marked `[CLOUD EXTENSION]` are
8
+ * exactly that gap.
9
+ *
10
+ * ```ts
11
+ * import { configure, redactAsync } from "euredact";
12
+ *
13
+ * configure({ apiKey: "erk_..." });
14
+ * const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
15
+ * ```
16
+ */
17
+ export { CloudClient, type CloudRedactOptions } from "./client.js";
18
+ export { configure, getConfig, reset, DEFAULT_BASE_URL, type CloudConfig, type ConfigureOptions, } from "./config.js";
19
+ export { CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, } from "./errors.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [CLOUD EXTENSION] Cloud tier — rules engine plus a fine-tuned model.
3
+ *
4
+ * The rule engine catches what has a shape: IBANs, national IDs, phone
5
+ * numbers, anything with a checksum. It cannot catch what does not — a
6
+ * person's name, an employer, a diagnosis, a job title. Those are what the
7
+ * cloud tier adds, and the `EntityType` members marked `[CLOUD EXTENSION]` are
8
+ * exactly that gap.
9
+ *
10
+ * ```ts
11
+ * import { configure, redactAsync } from "euredact";
12
+ *
13
+ * configure({ apiKey: "erk_..." });
14
+ * const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
15
+ * ```
16
+ */
17
+ export { CloudClient } from "./client.js";
18
+ export { configure, getConfig, reset, DEFAULT_BASE_URL, } from "./config.js";
19
+ export { CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, } from "./errors.js";
@@ -1,13 +1,21 @@
1
- export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES } from "./types.js";
1
+ export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES, canonicalType } from "./types.js";
2
2
  export type { Detection, RedactResult, PatternDef, CountryConfig, CountryEvidence } from "./types.js";
3
3
  export { DocumentContext } from "./rules/context.js";
4
4
  export { EuRedact, type RedactOptions } from "./sdk.js";
5
5
  export { COUNTRY_CONFIGS } from "./rules/countries/index.js";
6
6
  export { setBicRegistry, getBicRegistry, SEED_BIC6_PREFIXES, type BicRegistryProvider } from "./rules/bicRegistry.js";
7
+ export { configure, getConfig, CloudClient, CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, type CloudConfig, type ConfigureOptions, type CloudRedactOptions, } from "./cloud/index.js";
7
8
  import { type RedactOptions } from "./sdk.js";
8
9
  import type { RedactResult } from "./types.js";
9
10
  export declare function availableCountries(): string[];
10
11
  export declare function redact(text: string, options?: RedactOptions): RedactResult;
12
+ /**
13
+ * Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
14
+ *
15
+ * `mode: "rules"` resolves immediately with exactly what `redact()` returns,
16
+ * so a caller that may or may not use the cloud tier can hold one code path.
17
+ */
18
+ export declare function redactAsync(text: string, options?: RedactOptions): Promise<RedactResult>;
11
19
  export declare function addCustomPattern(name: string, pattern: string): void;
12
20
  export declare function clear(): void;
13
21
  export declare function redactBatch(texts: string[], options?: RedactOptions): RedactResult[];
package/dist/esm/index.js CHANGED
@@ -1,8 +1,9 @@
1
- export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES } from "./types.js";
1
+ export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES, canonicalType } from "./types.js";
2
2
  export { DocumentContext } from "./rules/context.js";
3
3
  export { EuRedact } from "./sdk.js";
4
4
  export { COUNTRY_CONFIGS } from "./rules/countries/index.js";
5
5
  export { setBicRegistry, getBicRegistry, SEED_BIC6_PREFIXES } from "./rules/bicRegistry.js";
6
+ export { configure, getConfig, CloudClient, CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, } from "./cloud/index.js";
6
7
  import { EuRedact } from "./sdk.js";
7
8
  import { COUNTRY_CONFIGS } from "./rules/countries/index.js";
8
9
  let _instance = null;
@@ -17,6 +18,15 @@ export function availableCountries() {
17
18
  export function redact(text, options) {
18
19
  return getInstance().redact(text, options);
19
20
  }
21
+ /**
22
+ * Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
23
+ *
24
+ * `mode: "rules"` resolves immediately with exactly what `redact()` returns,
25
+ * so a caller that may or may not use the cloud tier can hold one code path.
26
+ */
27
+ export function redactAsync(text, options) {
28
+ return getInstance().redactAsync(text, options);
29
+ }
20
30
  export function addCustomPattern(name, pattern) {
21
31
  getInstance().addCustomPattern(name, pattern);
22
32
  }
@@ -33,12 +33,28 @@
33
33
  * the *tail* of an ordinary word. "NIS" matched "tālru**nis**", the Latvian
34
34
  * for telephone, and suppressed 653 real phone numbers; "iva" matched
35
35
  * "Pr**iva**t:" and read a personal e-mail address as a VAT number.
36
- * * The qualifier class is spelled out rather than written `[^\W\d_]`, which
37
- * would reject "εταιρειας" here for the same ASCII reason.
36
+ * * Neither the qualifier nor the run-on may use `\w`. The qualifier is a
37
+ * negated ASCII class, fixed for this in 0.3.8; `[^\W\d_]` would reject
38
+ * "εταιρειας" here for the same ASCII reason. The run-on was *not* fixed then
39
+ * and went on diverging for another release: Slovak "telefón 0956550012"
40
+ * needs it to absorb "efón" after `tel`, and Bulgarian
41
+ * "пощенски кодове 4000-4999" needs "ове" after `пощенски код`. Python's
42
+ * Unicode `\w` did; this SDK's ASCII `\w` did not, so one engine said `PHONE`
43
+ * and the other `NATIONAL_ID` on the same document. They were the last two
44
+ * type divergences on the corpus, and only the type-aware half of
45
+ * `make parity` could see them.
38
46
  *
39
- * Every label ends in `(?:\w*|\s+<one word>)`: the label either runs on into a
40
- * longer word ("telefoon" → "telefoonnummer") **or** takes one qualifier word
41
- * naming whose identifier it is ("ΑΦΜ εταιρειας:", "Tel Nr:") — never both.
47
+ * The run-on is spelled `(?:[A-Za-z0-9_]|[^\x00-\x7F])` — an ASCII word
48
+ * character, or any non-ASCII character — rather than as a negated class of
49
+ * separators. A negated class also admits `'`, `%`, `&`, `@` and the rest of
50
+ * ASCII punctuation, which `\w` never did, and widening it that far was
51
+ * measured at 46 extra false positives with hints and 57 blind over the
52
+ * 152,300-document corpus. This is `\w` plus the non-ASCII letters `\w`
53
+ * should have matched all along, and nothing else.
54
+ *
55
+ * Every label ends in `(?:<run-on>|\s+<one word>)`: the label either runs on
56
+ * into a longer word ("telefoon" → "telefoonnummer") **or** takes one qualifier
57
+ * word naming whose identifier it is ("ΑΦΜ εταιρειας:", "Tel Nr:") — never both.
42
58
  * Allowing both let "nie" grow into the surname "Nieminen" and then swallow the
43
59
  * real label in "Matti Nieminen\nDOB: 08.05.1994", reading a date of birth as a
44
60
  * Spanish NIE: 52 false positives across the corpus.
@@ -49,7 +65,7 @@ import { EntityType } from "../types.js";
49
65
  * the value it introduces, which is the lesson of the postal-code year defect,
50
66
  * where a cue 150 characters away licensed every number in the window.
51
67
  */
52
- export declare const CUE_WINDOW = 22;
68
+ export declare const CUE_WINDOW = 32;
53
69
  /**
54
70
  * (entity type, label pattern). Ordered: the first match wins, so the narrower
55
71
  * label comes first where two could read the same text.
@@ -64,22 +80,33 @@ export declare const CUES: Array<[EntityType, RegExp]>;
64
80
  */
65
81
  export declare function cuedType(text: string, start: number): EntityType | null;
66
82
  /**
67
- * Types a label can assert on its own. Also the types eligible for the rescue
68
- * in `engine.ts`, for the same reason read the other way — a label may vouch
69
- * for one of these, so it may also vouch for a malformed one.
83
+ * Types a label can assert on its own.
70
84
  *
71
85
  * PHONE is absent on purpose: "Tel:" in front of something no phone pattern
72
86
  * matched means the document is laid out oddly, not that the value is a phone
73
- * number. So are EMAIL, SECRET, DOB, VIN and the rest — those carry their own
87
+ * number. So are EMAIL, DOB, VIN and the rest — those carry their own
74
88
  * structure, and a nearby word is not entitled to overrule it.
89
+ */
90
+ export declare const CUE_TARGETS: Set<string>;
91
+ /**
92
+ * Which of those a label may also *rescue* — re-admit after its checksum
93
+ * failed. Until 0.3.9 this was the same set as CUE_TARGETS and it cannot stay
94
+ * that way: re-typing a span the engine already decided to mask is cheap to be
95
+ * wrong about, while rescuing one asserts that a value which failed its check
96
+ * digit is a real identifier anyway.
75
97
  *
76
98
  * BIC's absence is what keeps the rescue honest. Its validator is a *registry
77
99
  * lookup*, not a checksum, so a failure means "no such bank", which no label can
78
100
  * talk you out of. Rescuing it read 34 unlisted codes behind a "BIC:" label as
79
101
  * real ones. The same reasoning keeps CREDIT_CARD and BANK_ACCOUNT out: Luhn and
80
102
  * mod-97 are strong enough that a failure really does mean "not one of these".
103
+ * BANK_ACCOUNT joined CUE_TARGETS in 0.3.9 so "sort code 20-45-91" stops being a
104
+ * LICENSE_PLATE, and is kept out here for exactly the 0.3.8 reason.
105
+ *
106
+ * HEALTHCARE_PROVIDER and SECRET are absent because neither is checksummed;
107
+ * there is no failed validation for a label to overrule in the first place.
81
108
  */
82
- export declare const CUE_TARGETS: Set<string>;
109
+ export declare const RESCUE_TARGETS: Set<string>;
83
110
  /**
84
111
  * The type a cue overrules `entityType` with, or null to leave it alone.
85
112
  *
@@ -87,5 +114,8 @@ export declare const CUE_TARGETS: Set<string>;
87
114
  * without them fired 52 times and got roughly 30 of those wrong — EMAIL read as
88
115
  * VAT, a date of birth as a phone number, a VIN as VAT — against 20 real fixes.
89
116
  * With them: 14 re-typings, every one of them PHONE to a structured identifier.
117
+ *
118
+ * `countryScore` is the document's support for the country the winning pattern
119
+ * came from; see RETYPABLE_UNCORROBORATED.
90
120
  */
91
- export declare function retypedBy(text: string, start: number, entityType: EntityType | string): EntityType | null;
121
+ export declare function retypedBy(text: string, start: number, entityType: EntityType | string, countryScore: number): EntityType | null;