gheim 0.1.1 → 0.1.3

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.
@@ -1,5 +1,10 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
3
8
 
4
9
  // src/core/sentinels.ts
5
10
  var LABEL_TO_TAG = {
@@ -228,8 +233,10 @@ class LocalDetector {
228
233
  chunkTokens;
229
234
  chunkOverlapTokens;
230
235
  batchSize;
236
+ actualDevice = null;
231
237
  _pipeline = null;
232
238
  _loadPromise = null;
239
+ _onProgress;
233
240
  constructor(opts = {}) {
234
241
  this.model = opts.model ?? DEFAULT_MODEL2;
235
242
  this.device = opts.device ?? "auto";
@@ -238,11 +245,13 @@ class LocalDetector {
238
245
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
239
246
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
240
247
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
248
+ this._onProgress = opts.onProgress;
241
249
  if (this.chunkOverlapTokens >= this.chunkTokens) {
242
250
  throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
243
251
  }
244
252
  if (opts.pipelineInstance) {
245
253
  this._pipeline = opts.pipelineInstance;
254
+ this.actualDevice = this.device === "auto" ? null : this.device;
246
255
  }
247
256
  }
248
257
  async load() {
@@ -251,24 +260,70 @@ class LocalDetector {
251
260
  if (this._loadPromise)
252
261
  return this._loadPromise;
253
262
  this._loadPromise = (async () => {
254
- let mod;
255
- try {
256
- mod = await import("@huggingface/transformers");
257
- } catch (e) {
258
- throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
263
+ const mod = await loadTransformersJs();
264
+ const sequence = await this._planDeviceSequence();
265
+ let lastErr;
266
+ for (let i = 0;i < sequence.length; i++) {
267
+ const dev = sequence[i] ?? "wasm";
268
+ try {
269
+ const pipe = await mod.pipeline("token-classification", this.model, {
270
+ device: dev,
271
+ dtype: this.dtype,
272
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
273
+ });
274
+ this._pipeline = pipe;
275
+ this.actualDevice = dev;
276
+ return pipe;
277
+ } catch (e) {
278
+ lastErr = e;
279
+ if (i < sequence.length - 1)
280
+ continue;
281
+ throw e;
282
+ }
259
283
  }
260
- const pipe = await mod.pipeline("token-classification", this.model, {
261
- device: this.device,
262
- dtype: this.dtype
263
- });
264
- this._pipeline = pipe;
265
- return pipe;
284
+ throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
266
285
  })();
267
286
  return this._loadPromise;
268
287
  }
288
+ async _planDeviceSequence() {
289
+ if (this.device !== "auto")
290
+ return [this.device];
291
+ const seq = [];
292
+ const nav = typeof navigator !== "undefined" ? navigator : null;
293
+ if (nav?.gpu?.requestAdapter) {
294
+ try {
295
+ const adapter = await nav.gpu.requestAdapter();
296
+ if (adapter)
297
+ seq.push("webgpu");
298
+ } catch {}
299
+ }
300
+ seq.push("wasm");
301
+ return seq;
302
+ }
303
+ _emitProgress(raw, device) {
304
+ if (!this._onProgress)
305
+ return;
306
+ const event = {
307
+ phase: raw.status ?? "progress",
308
+ device
309
+ };
310
+ if (raw.file)
311
+ event.file = raw.file;
312
+ if (raw.status === "progress") {
313
+ if (raw.loaded != null)
314
+ event.loadedBytes = raw.loaded;
315
+ if (raw.total != null)
316
+ event.totalBytes = raw.total;
317
+ if (raw.progress != null) {
318
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
319
+ event.fraction = Math.max(0, Math.min(1, norm));
320
+ }
321
+ }
322
+ this._onProgress(event);
323
+ }
269
324
  async detect(text, opts) {
270
325
  if (opts?.model && opts.model !== this.model) {
271
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
326
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
272
327
  }
273
328
  if (!text)
274
329
  return [];
@@ -277,7 +332,7 @@ class LocalDetector {
277
332
  }
278
333
  async detectBatch(texts, opts) {
279
334
  if (opts?.model && opts.model !== this.model) {
280
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
335
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
281
336
  }
282
337
  if (texts.length === 0)
283
338
  return [];
@@ -438,102 +493,225 @@ function chunkByChars(text, chunkChars, overlapChars) {
438
493
  function normalizeWord(word) {
439
494
  return word.replace(/##/g, "").trim();
440
495
  }
441
- // src/detectors/index.ts
442
- function defaultDetector() {
443
- const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
444
- return hasKey ? new RemoteDetector : new LocalDetector;
445
- }
446
-
447
- // src/normalizers.ts
448
- import { createRequire as createRequire2 } from "node:module";
449
- var _require = null;
450
- var _requireResolved = false;
451
- function _getRequire() {
452
- if (_requireResolved)
453
- return _require;
454
- _requireResolved = true;
496
+ async function loadTransformersJs() {
455
497
  try {
456
- _require = createRequire2(import.meta.url);
498
+ return await import("@huggingface/transformers");
457
499
  } catch {
458
- _require = null;
500
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
459
501
  }
460
- return _require;
461
502
  }
462
- function loadSync(name, errMsg) {
463
- const req = _getRequire();
464
- if (!req)
465
- throw new Error(errMsg);
466
- try {
467
- return req(name);
468
- } catch {
469
- throw new Error(errMsg);
503
+ // src/detectors/composite.ts
504
+ function luhnOk(s) {
505
+ const digits = [];
506
+ for (const c of s) {
507
+ if (c >= "0" && c <= "9")
508
+ digits.push(Number(c));
509
+ }
510
+ if (digits.length < 13 || digits.length > 19)
511
+ return false;
512
+ let total = 0;
513
+ for (let i = 0;i < digits.length; i++) {
514
+ let d = digits[digits.length - 1 - i] ?? 0;
515
+ if (i % 2 === 1) {
516
+ d *= 2;
517
+ if (d > 9)
518
+ d -= 9;
519
+ }
520
+ total += d;
470
521
  }
522
+ return total % 10 === 0;
471
523
  }
472
- function loadPhoneLib() {
473
- return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
524
+ function ibanChOk(raw) {
525
+ const s = raw.replace(/[\s-]/g, "");
526
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
527
+ return false;
528
+ }
529
+ const rearranged = s.slice(4) + s.slice(0, 4);
530
+ let numeric = "";
531
+ for (const c of rearranged) {
532
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
533
+ }
534
+ let rem = 0;
535
+ for (const ch of numeric) {
536
+ rem = (rem * 10 + Number(ch)) % 97;
537
+ }
538
+ return rem === 1;
474
539
  }
475
- function e164(opts = {}) {
476
- const lib = loadPhoneLib();
477
- return (s) => {
478
- try {
479
- const parsed = lib.parsePhoneNumberFromString(s, opts.region);
480
- if (!parsed || !parsed.isValid())
481
- return null;
482
- return parsed.format("E.164");
483
- } catch {
484
- return null;
485
- }
486
- };
540
+ function ahvOk(s) {
541
+ const digits = [];
542
+ for (const c of s) {
543
+ if (c >= "0" && c <= "9")
544
+ digits.push(Number(c));
545
+ }
546
+ if (digits.length !== 13)
547
+ return false;
548
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
549
+ return false;
550
+ let total = 0;
551
+ for (let i = 0;i < 12; i++) {
552
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
553
+ }
554
+ const check = (10 - total % 10) % 10;
555
+ return digits[12] === check;
487
556
  }
488
- function loadChrono(opts) {
489
- const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
490
- if (opts.locale) {
491
- const localeParser = mod[opts.locale];
492
- if (localeParser)
493
- return localeParser;
494
- }
495
- return mod;
557
+ function vatCheOk(s) {
558
+ const digits = [];
559
+ for (const c of s) {
560
+ if (c >= "0" && c <= "9")
561
+ digits.push(Number(c));
562
+ }
563
+ if (digits.length < 9)
564
+ return false;
565
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
566
+ let total = 0;
567
+ for (let i = 0;i < 8; i++) {
568
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
569
+ }
570
+ let expected = 11 - total % 11;
571
+ if (expected === 11)
572
+ expected = 0;
573
+ else if (expected === 10)
574
+ return false;
575
+ return digits[8] === expected;
496
576
  }
497
- function isoDate(opts = {}) {
498
- const parser = loadChrono(opts);
499
- return (s) => {
500
- const d = parser.parseDate(s);
501
- if (!d)
502
- return null;
503
- return d.toISOString().slice(0, 10);
504
- };
577
+ var REGEXES = [
578
+ {
579
+ label: "private_email",
580
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
581
+ },
582
+ {
583
+ label: "private_url",
584
+ pattern: /https?:\/\/[^\s)>\]]+/g
585
+ },
586
+ {
587
+ label: "private_phone",
588
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
589
+ },
590
+ {
591
+ label: "account_number",
592
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
593
+ validator: ibanChOk
594
+ },
595
+ {
596
+ label: "account_number",
597
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
598
+ validator: ahvOk
599
+ },
600
+ {
601
+ label: "account_number",
602
+ pattern: /\b756\d{10}\b/g,
603
+ validator: ahvOk
604
+ },
605
+ {
606
+ label: "account_number",
607
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
608
+ validator: vatCheOk
609
+ },
610
+ {
611
+ label: "account_number",
612
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
613
+ validator: luhnOk
614
+ },
615
+ {
616
+ label: "private_date",
617
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
618
+ },
619
+ {
620
+ label: "secret",
621
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
622
+ },
623
+ {
624
+ label: "secret",
625
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
626
+ },
627
+ {
628
+ label: "secret",
629
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
630
+ }
631
+ ];
632
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
633
+ function trimUrl(text, start, end) {
634
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
635
+ end--;
636
+ return [start, end];
505
637
  }
506
- function germanTransliteration() {
507
- const table = {
508
- ä: "ae",
509
- Ä: "ae",
510
- ö: "oe",
511
- Ö: "oe",
512
- ü: "ue",
513
- Ü: "ue",
514
- ß: "ss"
515
- };
516
- return (s) => {
517
- let out = "";
518
- for (const ch of s.normalize("NFKC")) {
519
- out += table[ch] ?? ch;
638
+ function findRegexSpans(text) {
639
+ const candidates = [];
640
+ for (const { label, pattern, validator } of REGEXES) {
641
+ pattern.lastIndex = 0;
642
+ let m;
643
+ while ((m = pattern.exec(text)) !== null) {
644
+ let start = m.index;
645
+ let end = m.index + m[0].length;
646
+ if (label === "private_url")
647
+ [start, end] = trimUrl(text, start, end);
648
+ const value = text.slice(start, end);
649
+ if (!value.trim())
650
+ continue;
651
+ if (validator && !validator(value))
652
+ continue;
653
+ candidates.push({
654
+ start,
655
+ end,
656
+ label,
657
+ text: value,
658
+ score: 1
659
+ });
520
660
  }
521
- return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
522
- };
661
+ }
662
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
663
+ const out = [];
664
+ let lastEnd = -1;
665
+ for (const sp of candidates) {
666
+ if (sp.start < lastEnd)
667
+ continue;
668
+ out.push(sp);
669
+ lastEnd = sp.end;
670
+ }
671
+ return out;
523
672
  }
524
- var BUILTINS = {
525
- e164: () => e164(),
526
- iso_date: () => isoDate(),
527
- german: () => germanTransliteration()
528
- };
529
- function resolveNormalizer(spec) {
530
- if (typeof spec === "function")
531
- return spec;
532
- const factory = BUILTINS[spec];
533
- if (!factory) {
534
- throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
535
- }
536
- return factory();
673
+ function maskSpans(text, spans) {
674
+ if (spans.length === 0)
675
+ return text;
676
+ const chars = [...text];
677
+ for (const sp of spans) {
678
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
679
+ chars[i] = " ";
680
+ }
681
+ }
682
+ return chars.join("");
683
+ }
684
+
685
+ class CompositeDetector {
686
+ model;
687
+ constructor(opts) {
688
+ if (!opts || !opts.model) {
689
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
690
+ }
691
+ this.model = opts.model;
692
+ }
693
+ async detect(text, opts) {
694
+ if (!text)
695
+ return [];
696
+ const regexSpans = findRegexSpans(text);
697
+ const masked = maskSpans(text, regexSpans);
698
+ const modelSpans = await this.model.detect(masked, opts);
699
+ const out = [...regexSpans];
700
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
701
+ for (const sp of modelSpans) {
702
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
703
+ if (overlaps)
704
+ continue;
705
+ out.push(sp);
706
+ }
707
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
708
+ return out;
709
+ }
710
+ }
711
+ // src/detectors/index.ts
712
+ function defaultDetector() {
713
+ const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
714
+ return hasKey ? new RemoteDetector : new LocalDetector;
537
715
  }
538
716
 
539
717
  // src/core/session.ts
@@ -548,8 +726,8 @@ class Session {
548
726
  resolvedNormalizers = {};
549
727
  constructor(opts = {}) {
550
728
  if (opts.normalizers) {
551
- for (const [label, spec] of Object.entries(opts.normalizers)) {
552
- this.resolvedNormalizers[label] = resolveNormalizer(spec);
729
+ for (const [label, fn] of Object.entries(opts.normalizers)) {
730
+ this.resolvedNormalizers[label] = fn;
553
731
  }
554
732
  }
555
733
  }
@@ -573,8 +751,11 @@ class Session {
573
751
  return sentinel;
574
752
  }
575
753
  applySpans(text, spans) {
754
+ return this.applySpansDetailed(text, spans).redacted;
755
+ }
756
+ applySpansDetailed(text, spans) {
576
757
  if (spans.length === 0)
577
- return text;
758
+ return { redacted: text, merged: [] };
578
759
  const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
579
760
  const merged = mergeAdjacent(text, sorted);
580
761
  const out = [];
@@ -590,7 +771,7 @@ class Session {
590
771
  cursor = end;
591
772
  }
592
773
  out.push(text.slice(cursor));
593
- return out.join("");
774
+ return { redacted: out.join(""), merged };
594
775
  }
595
776
  restore(text) {
596
777
  if (Object.keys(this.mapping).length === 0)
@@ -823,6 +1004,16 @@ function restoreChatResponse(response, session) {
823
1004
  if (msg && typeof msg.content === "string") {
824
1005
  msg.content = session.restore(msg.content);
825
1006
  }
1007
+ if (msg && typeof msg.reasoning === "string") {
1008
+ msg.reasoning = session.restore(msg.reasoning);
1009
+ }
1010
+ if (msg && Array.isArray(msg.reasoning_details)) {
1011
+ for (const r of msg.reasoning_details) {
1012
+ if (r && typeof r.text === "string") {
1013
+ r.text = session.restore(r.text);
1014
+ }
1015
+ }
1016
+ }
826
1017
  for (const tc of msg?.tool_calls ?? []) {
827
1018
  const fn = tc.function;
828
1019
  if (fn && typeof fn.arguments === "string") {
@@ -837,6 +1028,16 @@ function patchChatChunk(chunk, buf) {
837
1028
  if (typeof delta.content === "string") {
838
1029
  delta.content = buf.feed(delta.content);
839
1030
  }
1031
+ if (typeof delta.reasoning === "string") {
1032
+ delta.reasoning = buf.feed(delta.reasoning);
1033
+ }
1034
+ if (Array.isArray(delta.reasoning_details)) {
1035
+ for (const r of delta.reasoning_details) {
1036
+ if (r && typeof r.text === "string") {
1037
+ r.text = buf.feed(r.text);
1038
+ }
1039
+ }
1040
+ }
840
1041
  }
841
1042
  }
842
1043
  function makeChatTailChunk(template, content) {
@@ -1163,9 +1364,20 @@ class OpenAI {
1163
1364
  gheimStrict;
1164
1365
  _clientPromise;
1165
1366
  constructor(opts = {}) {
1166
- const detector = opts.gheimDetector ?? defaultDetector();
1167
- this.gheimStrict = opts.gheimStrict ?? true;
1168
- this._clientPromise = opts.openaiClient ? Promise.resolve(opts.openaiClient) : loadOpenAI(opts.clientOptions);
1367
+ const {
1368
+ gheimDetector,
1369
+ gheimStrict,
1370
+ openaiClient,
1371
+ clientOptions,
1372
+ ...passthrough
1373
+ } = opts;
1374
+ const detector = gheimDetector ?? defaultDetector();
1375
+ this.gheimStrict = gheimStrict ?? true;
1376
+ const innerOpts = {
1377
+ ...clientOptions ?? {},
1378
+ ...passthrough
1379
+ };
1380
+ this._clientPromise = openaiClient ? Promise.resolve(openaiClient) : loadOpenAI(innerOpts);
1169
1381
  this._clientPromise.then((c) => {
1170
1382
  this.raw = c;
1171
1383
  });
@@ -1,15 +1,24 @@
1
1
  /** Session state — mirrors gheim-py's Session. */
2
2
  import type { Span } from "../detectors/base.ts";
3
- import type { Normalizer } from "../normalizers.ts";
3
+ /** A surface canonicaliser. Maps a raw surface to a canonical key, or
4
+ * null to fall back to the default NFKC + lowercase key. Re-declared
5
+ * locally so this module doesn't pull in `gheim/normalizers` (which
6
+ * uses Node's `createRequire` and breaks browser bundlers). */
7
+ export type Normalizer = (s: string) => string | null;
4
8
  /** Constructor options for Session. */
5
9
  export interface SessionOptions {
6
10
  /**
7
11
  * Optional per-label canonical-form normalizers. Maps a label name
8
- * (e.g. "private_phone") to either a built-in name ("e164" /
9
- * "iso_date") or a callable Normalizer. Surfaces that fail to parse
10
- * fall back to the default NFKC+lowercase key. See ./normalizers.ts.
12
+ * (e.g. "private_phone") to a callable Normalizer. Surfaces that fail
13
+ * to parse fall back to the default NFKC+lowercase key.
14
+ *
15
+ * Built-in factories (e164, isoDate, germanTransliteration) live in
16
+ * the `gheim/normalizers` subpath — they pull in optional peer deps
17
+ * (`libphonenumber-js`, `chrono-node`) so they're not loaded by
18
+ * default to keep the browser bundle small and avoid Node-only
19
+ * imports leaking into the main entry.
11
20
  */
12
- normalizers?: Record<string, string | Normalizer>;
21
+ normalizers?: Record<string, Normalizer>;
13
22
  }
14
23
  export declare class Session {
15
24
  /** Sentinel string ("<PERSON_1>") → original surface form. */
@@ -19,7 +28,19 @@ export declare class Session {
19
28
  private resolvedNormalizers;
20
29
  constructor(opts?: SessionOptions);
21
30
  allocate(label: string, surface: string): string;
22
- applySpans(text: string, spans: Span[]): string;
31
+ applySpans(text: string, spans: readonly (Span | MergedSpan)[]): string;
32
+ /**
33
+ * Like {@link applySpans} but also returns the post-merge span list. UI
34
+ * consumers (segment rendering, bar overlays) typically need both the
35
+ * redacted text and the merged spans so they can highlight the redacted
36
+ * runs at their original character positions; without this method they'd
37
+ * have to call {@link mergeAdjacent} again on the same input. Pure
38
+ * convenience — no extra cost over `applySpans`.
39
+ */
40
+ applySpansDetailed(text: string, spans: readonly (Span | MergedSpan)[]): {
41
+ redacted: string;
42
+ merged: MergedSpan[];
43
+ };
23
44
  restore(text: string): string;
24
45
  toJSON(): {
25
46
  mapping: Record<string, string>;
@@ -28,4 +49,19 @@ export declare class Session {
28
49
  mapping: Record<string, string>;
29
50
  }, opts?: SessionOptions): Session;
30
51
  }
52
+ /** A span after merge: same shape as input, but no `text`/`score` carried. */
53
+ export interface MergedSpan {
54
+ start: number;
55
+ end: number;
56
+ label: string;
57
+ }
58
+ /** Merge runs of adjacent same-label spans (BPE-split surface fix).
59
+ *
60
+ * Two same-label spans separated by ≤ MERGE_GAP characters of pure
61
+ * whitespace (or directly touching) collapse into one. Models tokenize
62
+ * a phone like "+41 44 268 12 34" into many single-space-separated
63
+ * pieces with the same label; merging recovers the original entity so
64
+ * the demo and SDK both see one PHONE span instead of five.
65
+ */
66
+ export declare function mergeAdjacent(text: string, spans: readonly (Span | MergedSpan)[]): MergedSpan[];
31
67
  //# sourceMappingURL=session.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAmBpD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC;CACnD;AAED,qBAAa,OAAO;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IACrC,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,mBAAmB,CAAkC;gBAEjD,IAAI,GAAE,cAAmB;IAQrC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAiBhD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM;IAoB/C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK7B,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAI7C,MAAM,CAAC,QAAQ,CACb,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACxC,IAAI,GAAE,cAAmB,GACxB,OAAO;CAqBX"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;gEAGgE;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAkBtD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAED,qBAAa,OAAO;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IACrC,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,mBAAmB,CAAkC;gBAEjD,IAAI,GAAE,cAAmB;IAQrC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAiBhD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GAAG,MAAM;IAIvE;;;;;;;OAOG;IACH,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;IAoB7C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK7B,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAI7C,MAAM,CAAC,QAAQ,CACb,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACxC,IAAI,GAAE,cAAmB,GACxB,OAAO;CAqBX;AAID,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC,UAAU,EAAE,CAqBd"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Composite detector: regex front-end + model on the remainder.
3
+ *
4
+ * Production-ready Swiss PII detector that combines two complementary
5
+ * strategies — a port of the Python sibling
6
+ * (`gheim.detectors.composite.CompositeDetector`) with parity-tested
7
+ * regexes and checksum validators.
8
+ *
9
+ * 1. Deterministic regex front-end for structured PII categories where
10
+ * shape is unambiguous and checksums verify identity:
11
+ * - IBAN-CH (mod-97-10), AHV (EAN-13 mod-10), VAT-CHE (mod-11-10),
12
+ * Visa/Mastercard (Luhn)
13
+ * - Email, URL, Swiss phone (+41 / 0041 / 0...), explicit dates
14
+ * (DD.MM.YYYY, YYYY-MM-DD)
15
+ * - API tokens (sk-, ghp_, AKIA...)
16
+ *
17
+ * Spans found here are "trusted" — checksums catch most surface-form
18
+ * imitations; the model is never asked.
19
+ *
20
+ * 2. The fine-tuned model on whatever's left after regex masking.
21
+ * Persons, addresses, and edge-case dates fall here. The model's
22
+ * "single-category mode" failure (where an IBAN/phone-shaped
23
+ * sequence suppresses person/date detection in the same input) is
24
+ * bypassed because the regex already absorbed those tokens — the
25
+ * model sees them as whitespace.
26
+ *
27
+ * Wraps the same `Detector` interface (`detect(text) → Span[]`) as
28
+ * `LocalDetector` so it drops into any code path that already accepts a
29
+ * detector.
30
+ */
31
+ import type { Detector, Span } from "./base.ts";
32
+ /** Options for {@link CompositeDetector}. */
33
+ export interface CompositeDetectorOptions {
34
+ /** Inner detector — usually a {@link LocalDetector}. Anything with
35
+ * a `detect(text) → Promise<Span[]>` method works. */
36
+ model: Detector;
37
+ }
38
+ /** Regex front-end (with checksum validation) + model on the remainder.
39
+ *
40
+ * @example
41
+ * import { LocalDetector, CompositeDetector } from "gheim";
42
+ * const composite = new CompositeDetector({
43
+ * model: new LocalDetector({ dtype: "q8" }),
44
+ * });
45
+ * const spans = await composite.detect(swissLetter);
46
+ * // → IBAN/phone/email caught by regex (checksum-validated);
47
+ * // person/address/dates caught by the model on the masked remainder.
48
+ */
49
+ export declare class CompositeDetector implements Detector {
50
+ readonly model: Detector;
51
+ constructor(opts: CompositeDetectorOptions);
52
+ /** Two-pass detection: regex first (with checksum), then model on
53
+ * the masked remainder. Returns a single overlap-resolved span list. */
54
+ detect(text: string, opts?: {
55
+ model?: string;
56
+ }): Promise<Span[]>;
57
+ }
58
+ //# sourceMappingURL=composite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/detectors/composite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiNhD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;2DACuD;IACvD,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAEb,IAAI,EAAE,wBAAwB;IAU1C;6EACyE;IACnE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAmBvE"}