@floatingsidewal/bulkhead-core 0.3.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 @@
1
+ {"version":3,"sources":["../../src/cascade/index.ts","../../src/cascade/bert-layer.ts","../../src/cascade/llm-layer.ts","../../src/cascade/cascade.ts"],"sourcesContent":["// Advanced cascade internals — most consumers should use createEngine() instead\nexport { CascadeClassifier } from \"./cascade\";\nexport { BertLayer } from \"./bert-layer\";\nexport { LlmLayer } from \"./llm-layer\";\nexport type { CascadeConfig } from \"./cascade\";\nexport type { BertLayerConfig } from \"./bert-layer\";\nexport type { LlmProvider, LlmLayerConfig } from \"./llm-layer\";\n","/**\n * Main-thread interface to the BERT worker (Layer 2).\n * Manages the worker lifecycle and maps BERT tokens to Detection objects.\n */\n\nimport { Worker } from \"node:worker_threads\";\nimport { resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport type { Detection, Confidence } from \"../types\";\nimport type { WorkerRequest, WorkerResponse, BertToken } from \"./bert-worker\";\n\nconst DEFAULT_MODEL_ID = \"Xenova/bert-base-NER\";\n\nexport interface BertLayerConfig {\n modelId?: string;\n /** Threshold above which detections are confirmed, below which they escalate */\n escalationThreshold: number;\n}\n\nexport class BertLayer {\n private worker: Worker | null = null;\n private pendingRequests = new Map<\n string,\n { resolve: (tokens: BertToken[]) => void; reject: (err: Error) => void }\n >();\n private requestId = 0;\n private config: BertLayerConfig;\n\n /** Whether the BERT model has been loaded and first inference completed */\n private _loaded = false;\n get loaded(): boolean {\n return this._loaded;\n }\n\n constructor(config?: Partial<BertLayerConfig>) {\n this.config = {\n escalationThreshold: 0.75,\n ...config,\n };\n }\n\n /** Resolve the worker path — supports both compiled .js and source .ts */\n private resolveWorkerPath(): string {\n // Compiled: dist/cascade/bert-worker.js (built as separate tsup entry)\n const compiledPath = resolve(__dirname, \"cascade\", \"bert-worker.js\");\n if (existsSync(compiledPath)) return compiledPath;\n\n // Dev/test mode: use TypeScript source with tsx loader\n const tsPath = resolve(__dirname, \"bert-worker.ts\");\n if (existsSync(tsPath)) return tsPath;\n\n return compiledPath; // Fall back to compiled path (will error if missing)\n }\n\n /** Ensure the worker thread is running */\n private ensureWorker(): Worker {\n if (!this.worker) {\n const workerPath = this.resolveWorkerPath();\n const isTs = workerPath.endsWith(\".ts\");\n\n // In dev/test mode (.ts), use tsx to transpile the worker on the fly\n this.worker = isTs\n ? new Worker(workerPath, {\n execArgv: [\"--require\", \"tsx/cjs\"],\n })\n : new Worker(workerPath);\n\n this.worker.on(\"message\", (msg: WorkerResponse) => {\n const pending = this.pendingRequests.get(msg.id);\n if (!pending) return;\n\n this.pendingRequests.delete(msg.id);\n if (msg.type === \"error\") {\n pending.reject(new Error(msg.error ?? \"Unknown worker error\"));\n } else {\n pending.resolve(msg.tokens ?? []);\n }\n });\n\n this.worker.on(\"error\", (err) => {\n // Reject all pending requests\n for (const [id, pending] of this.pendingRequests) {\n pending.reject(err);\n this.pendingRequests.delete(id);\n }\n });\n }\n return this.worker;\n }\n\n /** Send text to the BERT worker and get raw token results */\n private async analyzeRaw(text: string): Promise<BertToken[]> {\n const worker = this.ensureWorker();\n const id = String(++this.requestId);\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, { resolve, reject });\n worker.postMessage({\n type: \"analyze\",\n id,\n text,\n modelId: this.config.modelId ?? DEFAULT_MODEL_ID,\n } as WorkerRequest);\n });\n }\n\n /**\n * Analyze text and return Detection objects with escalation disposition.\n * Tokens above the escalation threshold are \"confirmed\",\n * tokens below are \"escalate\" (need LLM review).\n */\n async analyze(text: string): Promise<Detection[]> {\n const tokens = await this.analyzeRaw(text);\n this._loaded = true;\n\n return tokens.map((token) => {\n // Strip B-/I- prefix from entity labels (e.g., \"B-PERSON\" → \"PERSON\")\n const entityType = token.entity.replace(/^[BI]-/, \"\");\n const isConfirmed = token.score >= this.config.escalationThreshold;\n\n const confidence: Confidence =\n token.score >= 0.9 ? \"high\" : token.score >= 0.7 ? \"medium\" : \"low\";\n\n return {\n entityType,\n start: token.start,\n end: token.end,\n text: token.word,\n confidence,\n score: token.score,\n guardName: \"cascade-bert\",\n source: \"bert\" as const,\n context: text.slice(\n Math.max(0, token.start - 150),\n Math.min(text.length, token.end + 150)\n ),\n disposition: isConfirmed ? (\"confirmed\" as const) : (\"escalate\" as const),\n };\n });\n }\n\n /** Terminate the worker thread */\n async dispose(): Promise<void> {\n if (this.worker) {\n await this.worker.terminate();\n this.worker = null;\n this.pendingRequests.clear();\n }\n }\n}\n","/**\n * LLM disambiguation layer (Layer 3) of the cascading classifier.\n * Only receives ambiguous spans from Layer 2, along with surrounding context.\n * Makes a focused determination: is this span PII or not?\n */\n\nimport type { Detection, Disposition } from \"../types\";\n\n/** Function signature for an LLM provider */\nexport type LlmProvider = (prompt: string) => Promise<string>;\n\nexport interface LlmLayerConfig {\n /** Number of sentences before/after the span to include as context */\n contextSentences: number;\n /** LLM provider function */\n provider?: LlmProvider;\n}\n\ninterface DisambiguationResult {\n type: string;\n confidence: number;\n}\n\nexport class LlmLayer {\n private config: LlmLayerConfig;\n\n constructor(config?: Partial<LlmLayerConfig>) {\n this.config = {\n contextSentences: 3,\n ...config,\n };\n }\n\n /** Set the LLM provider (can be swapped at runtime) */\n setProvider(provider: LlmProvider): void {\n this.config.provider = provider;\n }\n\n /**\n * Disambiguate escalated detections using an LLM.\n * @param escalated Detections with disposition \"escalate\"\n * @param fullText The full document text\n * @param confirmed Already-confirmed detections (passed as context to help disambiguation)\n */\n async disambiguate(\n escalated: Detection[],\n fullText: string,\n confirmed: Detection[]\n ): Promise<Detection[]> {\n if (!this.config.provider) {\n // No LLM configured — return escalated items as-is\n return escalated;\n }\n\n const results: Detection[] = [];\n\n for (const detection of escalated) {\n const prompt = this.buildPrompt(detection, fullText, confirmed);\n\n try {\n const response = await this.config.provider(prompt);\n const parsed = this.parseResponse(response);\n\n if (parsed && parsed.type !== \"NONE\") {\n results.push({\n ...detection,\n entityType: parsed.type,\n score: parsed.confidence,\n confidence:\n parsed.confidence >= 0.9\n ? \"high\"\n : parsed.confidence >= 0.7\n ? \"medium\"\n : \"low\",\n source: \"llm\",\n disposition: \"confirmed\" as Disposition,\n });\n } else {\n results.push({\n ...detection,\n source: \"llm\",\n disposition: \"dismissed\" as Disposition,\n });\n }\n } catch {\n // LLM call failed — keep as escalated rather than losing data\n results.push(detection);\n }\n }\n\n return results;\n }\n\n /** Build a focused disambiguation prompt */\n private buildPrompt(\n detection: Detection,\n fullText: string,\n confirmed: Detection[]\n ): string {\n const contextWindow = this.extractSentenceContext(\n fullText,\n detection.start,\n detection.end\n );\n\n const confirmedList = confirmed\n .filter((d) => d.disposition === \"confirmed\")\n .map((d) => `${d.text} (${d.entityType})`)\n .slice(0, 10); // Limit to avoid excessive token use\n\n return `You are a PII detection system. Determine if the highlighted span is personally identifiable information.\n\nContext: \"${contextWindow}\"\nSpan: \"${detection.text}\"\nBERT suggested: ${detection.entityType} (confidence: ${detection.score.toFixed(2)})\n${confirmedList.length > 0 ? `Other confirmed entities in document: [${confirmedList.join(\", \")}]` : \"\"}\n\nIs this span PII? If yes, what type? If it's ambiguous (e.g., \"Jordan\" could be a person or country), use the context to decide.\n\nRespond with ONLY a JSON object: { \"type\": \"PERSON\"|\"LOCATION\"|\"ORGANIZATION\"|\"NONE\", \"confidence\": 0.0-1.0 }`;\n }\n\n /** Extract ±N sentences around a span */\n private extractSentenceContext(\n text: string,\n start: number,\n end: number\n ): string {\n const n = this.config.contextSentences;\n\n // Split into sentences (simple heuristic)\n const sentenceBreaks: number[] = [0];\n const sentenceRegex = /[.!?]+\\s+/g;\n let match: RegExpExecArray | null;\n while ((match = sentenceRegex.exec(text)) !== null) {\n sentenceBreaks.push(match.index + match[0].length);\n }\n sentenceBreaks.push(text.length);\n\n // Find which sentence contains the span\n let spanSentenceIdx = 0;\n for (let i = 0; i < sentenceBreaks.length - 1; i++) {\n if (sentenceBreaks[i] <= start && start < sentenceBreaks[i + 1]) {\n spanSentenceIdx = i;\n break;\n }\n }\n\n // Extract ±n sentences\n const contextStart =\n sentenceBreaks[Math.max(0, spanSentenceIdx - n)];\n const contextEnd =\n sentenceBreaks[\n Math.min(sentenceBreaks.length - 1, spanSentenceIdx + n + 1)\n ];\n\n return text.slice(contextStart, contextEnd).trim();\n }\n\n /** Parse the LLM response JSON */\n private parseResponse(response: string): DisambiguationResult | null {\n try {\n // Extract JSON from response (may have surrounding text)\n const jsonMatch = response.match(/\\{[^}]+\\}/);\n if (!jsonMatch) return null;\n\n const parsed = JSON.parse(jsonMatch[0]);\n if (\n typeof parsed.type === \"string\" &&\n typeof parsed.confidence === \"number\"\n ) {\n return parsed as DisambiguationResult;\n }\n return null;\n } catch {\n return null;\n }\n }\n}\n","/**\n * Cascading Classifier — orchestrates the three detection layers.\n *\n * Layer 1 (Regex): Always runs, sub-ms. Catches structured PII.\n * → confidence: 1.0, disposition: \"confirmed\"\n *\n * Layer 2 (BERT): On-demand, 20-50ms. Catches contextual entities.\n * → score >= threshold: \"confirmed\"\n * → score < threshold: \"escalate\"\n *\n * Layer 3 (LLM): Selective, 500ms-2s. Only sees escalated spans.\n * → Returns \"confirmed\" or \"dismissed\"\n */\n\nimport type { Detection, GuardResult } from \"../types\";\nimport type { Guard } from \"../types\";\nimport { BertLayer, type BertLayerConfig } from \"./bert-layer\";\nimport { LlmLayer, type LlmProvider, type LlmLayerConfig } from \"./llm-layer\";\n\nexport interface CascadeConfig {\n /** Confidence threshold below which BERT results escalate to LLM */\n escalationThreshold: number;\n /** Number of sentences of context to pass to Layer 3 */\n contextSentences: number;\n /** Whether Layer 2 (BERT) is enabled */\n bertEnabled: boolean;\n /** Whether Layer 3 (LLM) is enabled */\n llmEnabled: boolean;\n /** Model ID for BERT layer */\n modelId?: string;\n /** LLM provider function for Layer 3 */\n llmProvider?: LlmProvider;\n}\n\nconst DEFAULT_CASCADE_CONFIG: CascadeConfig = {\n escalationThreshold: 0.75,\n contextSentences: 3,\n bertEnabled: true,\n llmEnabled: false,\n modelId: \"Xenova/bert-base-NER\",\n};\n\nexport class CascadeClassifier {\n private config: CascadeConfig;\n private bertLayer: BertLayer | null = null;\n private llmLayer: LlmLayer;\n private regexGuards: Guard[] = [];\n\n constructor(config?: Partial<CascadeConfig>) {\n this.config = { ...DEFAULT_CASCADE_CONFIG, ...config };\n this.llmLayer = new LlmLayer({\n contextSentences: this.config.contextSentences,\n provider: this.config.llmProvider,\n });\n }\n\n /** Whether the cascade is ready to serve (BERT model loaded if enabled) */\n get ready(): boolean {\n if (!this.config.bertEnabled) return true;\n if (!this.bertLayer) return true; // Not yet initialized, will lazy-load\n return this.bertLayer.loaded;\n }\n\n /** Register regex-based guards (Layer 1) */\n addRegexGuard(guard: Guard): this {\n this.regexGuards.push(guard);\n return this;\n }\n\n /** Set the LLM provider for Layer 3 */\n setLlmProvider(provider: LlmProvider): void {\n this.config.llmProvider = provider;\n this.llmLayer.setProvider(provider);\n }\n\n /**\n * Run the full cascade: Regex → BERT → LLM\n * Returns a unified GuardResult with all detections carrying provenance.\n */\n async deepScan(text: string): Promise<GuardResult> {\n // === Layer 1: Regex (always) ===\n const regexDetections = await this.runRegexLayer(text);\n\n // If BERT is disabled, return regex-only results\n if (!this.config.bertEnabled) {\n return this.buildCascadeResult(text, regexDetections);\n }\n\n // === Layer 2: BERT ===\n const bertDetections = await this.runBertLayer(text);\n\n // Deduplicate: regex wins for overlapping spans\n const mergedBert = this.deduplicateAgainstRegex(\n bertDetections,\n regexDetections\n );\n\n const allDetections = [...regexDetections, ...mergedBert];\n\n // If LLM is disabled or no escalated items, return here\n const escalated = allDetections.filter((d) => d.disposition === \"escalate\");\n if (!this.config.llmEnabled || escalated.length === 0 || !this.config.llmProvider) {\n return this.buildCascadeResult(text, allDetections);\n }\n\n // === Layer 3: LLM (only escalated spans) ===\n const confirmed = allDetections.filter((d) => d.disposition === \"confirmed\");\n const resolved = await this.llmLayer.disambiguate(\n escalated,\n text,\n confirmed\n );\n\n // Replace escalated detections with resolved ones\n const finalDetections = [\n ...allDetections.filter((d) => d.disposition !== \"escalate\"),\n ...resolved,\n ];\n\n return this.buildCascadeResult(text, finalDetections);\n }\n\n /** Run Layer 1 only (for fast auto-scan path) */\n async regexScan(text: string): Promise<GuardResult> {\n const detections = await this.runRegexLayer(text);\n return this.buildCascadeResult(text, detections);\n }\n\n /** Run Layers 1 + 2 only (no LLM, for \"Scan File\" command) */\n async modelScan(text: string): Promise<GuardResult> {\n const regexDetections = await this.runRegexLayer(text);\n if (!this.config.bertEnabled) {\n return this.buildCascadeResult(text, regexDetections);\n }\n\n const bertDetections = await this.runBertLayer(text);\n const mergedBert = this.deduplicateAgainstRegex(\n bertDetections,\n regexDetections\n );\n\n return this.buildCascadeResult(text, [...regexDetections, ...mergedBert]);\n }\n\n // --- Private methods ---\n\n private async runRegexLayer(text: string): Promise<Detection[]> {\n const allDetections: Detection[] = [];\n for (const guard of this.regexGuards) {\n const result = await guard.analyze(text);\n allDetections.push(...result.detections);\n }\n return allDetections;\n }\n\n private async runBertLayer(text: string): Promise<Detection[]> {\n if (!this.bertLayer) {\n this.bertLayer = new BertLayer({\n modelId: this.config.modelId,\n escalationThreshold: this.config.escalationThreshold,\n });\n }\n return this.bertLayer.analyze(text);\n }\n\n /** Remove BERT detections that overlap with regex detections */\n private deduplicateAgainstRegex(\n bertDetections: Detection[],\n regexDetections: Detection[]\n ): Detection[] {\n return bertDetections.filter((bert) => {\n return !regexDetections.some(\n (regex) => bert.start < regex.end && bert.end > regex.start\n );\n });\n }\n\n private buildCascadeResult(\n text: string,\n detections: Detection[]\n ): GuardResult {\n // Only count non-dismissed detections as failures\n const activeDetections = detections.filter(\n (d) => d.disposition !== \"dismissed\"\n );\n const passed = activeDetections.length === 0;\n const score =\n activeDetections.length > 0\n ? Math.max(...activeDetections.map((d) => d.score))\n : 0;\n\n const sources = [...new Set(detections.map((d) => d.source))];\n const types = [...new Set(activeDetections.map((d) => d.entityType))];\n\n return {\n passed,\n reason: passed\n ? \"No issues detected\"\n : `Detected via ${sources.join(\"+\")}: ${types.join(\", \")}`,\n guardName: \"cascade\",\n score,\n detections,\n };\n }\n\n /** Clean up resources */\n async dispose(): Promise<void> {\n if (this.bertLayer) {\n await this.bertLayer.dispose();\n this.bertLayer = null;\n }\n }\n}\n"],"mappings":"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","names":["resolve"]}
@@ -0,0 +1,11 @@
1
+ import {
2
+ BertLayer,
3
+ CascadeClassifier,
4
+ LlmLayer
5
+ } from "../chunk-4KUXRYNS.mjs";
6
+ export {
7
+ BertLayer,
8
+ CascadeClassifier,
9
+ LlmLayer
10
+ };
11
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,358 @@
1
+ // src/cascade/bert-layer.ts
2
+ import { Worker } from "worker_threads";
3
+ import { resolve } from "path";
4
+ import { existsSync } from "fs";
5
+ var DEFAULT_MODEL_ID = "Xenova/bert-base-NER";
6
+ var BertLayer = class {
7
+ worker = null;
8
+ pendingRequests = /* @__PURE__ */ new Map();
9
+ requestId = 0;
10
+ config;
11
+ /** Whether the BERT model has been loaded and first inference completed */
12
+ _loaded = false;
13
+ get loaded() {
14
+ return this._loaded;
15
+ }
16
+ constructor(config) {
17
+ this.config = {
18
+ escalationThreshold: 0.75,
19
+ ...config
20
+ };
21
+ }
22
+ /** Resolve the worker path — supports both compiled .js and source .ts */
23
+ resolveWorkerPath() {
24
+ const compiledPath = resolve(__dirname, "cascade", "bert-worker.js");
25
+ if (existsSync(compiledPath)) return compiledPath;
26
+ const tsPath = resolve(__dirname, "bert-worker.ts");
27
+ if (existsSync(tsPath)) return tsPath;
28
+ return compiledPath;
29
+ }
30
+ /** Ensure the worker thread is running */
31
+ ensureWorker() {
32
+ if (!this.worker) {
33
+ const workerPath = this.resolveWorkerPath();
34
+ const isTs = workerPath.endsWith(".ts");
35
+ this.worker = isTs ? new Worker(workerPath, {
36
+ execArgv: ["--require", "tsx/cjs"]
37
+ }) : new Worker(workerPath);
38
+ this.worker.on("message", (msg) => {
39
+ const pending = this.pendingRequests.get(msg.id);
40
+ if (!pending) return;
41
+ this.pendingRequests.delete(msg.id);
42
+ if (msg.type === "error") {
43
+ pending.reject(new Error(msg.error ?? "Unknown worker error"));
44
+ } else {
45
+ pending.resolve(msg.tokens ?? []);
46
+ }
47
+ });
48
+ this.worker.on("error", (err) => {
49
+ for (const [id, pending] of this.pendingRequests) {
50
+ pending.reject(err);
51
+ this.pendingRequests.delete(id);
52
+ }
53
+ });
54
+ }
55
+ return this.worker;
56
+ }
57
+ /** Send text to the BERT worker and get raw token results */
58
+ async analyzeRaw(text) {
59
+ const worker = this.ensureWorker();
60
+ const id = String(++this.requestId);
61
+ return new Promise((resolve2, reject) => {
62
+ this.pendingRequests.set(id, { resolve: resolve2, reject });
63
+ worker.postMessage({
64
+ type: "analyze",
65
+ id,
66
+ text,
67
+ modelId: this.config.modelId ?? DEFAULT_MODEL_ID
68
+ });
69
+ });
70
+ }
71
+ /**
72
+ * Analyze text and return Detection objects with escalation disposition.
73
+ * Tokens above the escalation threshold are "confirmed",
74
+ * tokens below are "escalate" (need LLM review).
75
+ */
76
+ async analyze(text) {
77
+ const tokens = await this.analyzeRaw(text);
78
+ this._loaded = true;
79
+ return tokens.map((token) => {
80
+ const entityType = token.entity.replace(/^[BI]-/, "");
81
+ const isConfirmed = token.score >= this.config.escalationThreshold;
82
+ const confidence = token.score >= 0.9 ? "high" : token.score >= 0.7 ? "medium" : "low";
83
+ return {
84
+ entityType,
85
+ start: token.start,
86
+ end: token.end,
87
+ text: token.word,
88
+ confidence,
89
+ score: token.score,
90
+ guardName: "cascade-bert",
91
+ source: "bert",
92
+ context: text.slice(
93
+ Math.max(0, token.start - 150),
94
+ Math.min(text.length, token.end + 150)
95
+ ),
96
+ disposition: isConfirmed ? "confirmed" : "escalate"
97
+ };
98
+ });
99
+ }
100
+ /** Terminate the worker thread */
101
+ async dispose() {
102
+ if (this.worker) {
103
+ await this.worker.terminate();
104
+ this.worker = null;
105
+ this.pendingRequests.clear();
106
+ }
107
+ }
108
+ };
109
+
110
+ // src/cascade/llm-layer.ts
111
+ var LlmLayer = class {
112
+ config;
113
+ constructor(config) {
114
+ this.config = {
115
+ contextSentences: 3,
116
+ ...config
117
+ };
118
+ }
119
+ /** Set the LLM provider (can be swapped at runtime) */
120
+ setProvider(provider) {
121
+ this.config.provider = provider;
122
+ }
123
+ /**
124
+ * Disambiguate escalated detections using an LLM.
125
+ * @param escalated Detections with disposition "escalate"
126
+ * @param fullText The full document text
127
+ * @param confirmed Already-confirmed detections (passed as context to help disambiguation)
128
+ */
129
+ async disambiguate(escalated, fullText, confirmed) {
130
+ if (!this.config.provider) {
131
+ return escalated;
132
+ }
133
+ const results = [];
134
+ for (const detection of escalated) {
135
+ const prompt = this.buildPrompt(detection, fullText, confirmed);
136
+ try {
137
+ const response = await this.config.provider(prompt);
138
+ const parsed = this.parseResponse(response);
139
+ if (parsed && parsed.type !== "NONE") {
140
+ results.push({
141
+ ...detection,
142
+ entityType: parsed.type,
143
+ score: parsed.confidence,
144
+ confidence: parsed.confidence >= 0.9 ? "high" : parsed.confidence >= 0.7 ? "medium" : "low",
145
+ source: "llm",
146
+ disposition: "confirmed"
147
+ });
148
+ } else {
149
+ results.push({
150
+ ...detection,
151
+ source: "llm",
152
+ disposition: "dismissed"
153
+ });
154
+ }
155
+ } catch {
156
+ results.push(detection);
157
+ }
158
+ }
159
+ return results;
160
+ }
161
+ /** Build a focused disambiguation prompt */
162
+ buildPrompt(detection, fullText, confirmed) {
163
+ const contextWindow = this.extractSentenceContext(
164
+ fullText,
165
+ detection.start,
166
+ detection.end
167
+ );
168
+ const confirmedList = confirmed.filter((d) => d.disposition === "confirmed").map((d) => `${d.text} (${d.entityType})`).slice(0, 10);
169
+ return `You are a PII detection system. Determine if the highlighted span is personally identifiable information.
170
+
171
+ Context: "${contextWindow}"
172
+ Span: "${detection.text}"
173
+ BERT suggested: ${detection.entityType} (confidence: ${detection.score.toFixed(2)})
174
+ ${confirmedList.length > 0 ? `Other confirmed entities in document: [${confirmedList.join(", ")}]` : ""}
175
+
176
+ Is this span PII? If yes, what type? If it's ambiguous (e.g., "Jordan" could be a person or country), use the context to decide.
177
+
178
+ Respond with ONLY a JSON object: { "type": "PERSON"|"LOCATION"|"ORGANIZATION"|"NONE", "confidence": 0.0-1.0 }`;
179
+ }
180
+ /** Extract ±N sentences around a span */
181
+ extractSentenceContext(text, start, end) {
182
+ const n = this.config.contextSentences;
183
+ const sentenceBreaks = [0];
184
+ const sentenceRegex = /[.!?]+\s+/g;
185
+ let match;
186
+ while ((match = sentenceRegex.exec(text)) !== null) {
187
+ sentenceBreaks.push(match.index + match[0].length);
188
+ }
189
+ sentenceBreaks.push(text.length);
190
+ let spanSentenceIdx = 0;
191
+ for (let i = 0; i < sentenceBreaks.length - 1; i++) {
192
+ if (sentenceBreaks[i] <= start && start < sentenceBreaks[i + 1]) {
193
+ spanSentenceIdx = i;
194
+ break;
195
+ }
196
+ }
197
+ const contextStart = sentenceBreaks[Math.max(0, spanSentenceIdx - n)];
198
+ const contextEnd = sentenceBreaks[Math.min(sentenceBreaks.length - 1, spanSentenceIdx + n + 1)];
199
+ return text.slice(contextStart, contextEnd).trim();
200
+ }
201
+ /** Parse the LLM response JSON */
202
+ parseResponse(response) {
203
+ try {
204
+ const jsonMatch = response.match(/\{[^}]+\}/);
205
+ if (!jsonMatch) return null;
206
+ const parsed = JSON.parse(jsonMatch[0]);
207
+ if (typeof parsed.type === "string" && typeof parsed.confidence === "number") {
208
+ return parsed;
209
+ }
210
+ return null;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ };
216
+
217
+ // src/cascade/cascade.ts
218
+ var DEFAULT_CASCADE_CONFIG = {
219
+ escalationThreshold: 0.75,
220
+ contextSentences: 3,
221
+ bertEnabled: true,
222
+ llmEnabled: false,
223
+ modelId: "Xenova/bert-base-NER"
224
+ };
225
+ var CascadeClassifier = class {
226
+ config;
227
+ bertLayer = null;
228
+ llmLayer;
229
+ regexGuards = [];
230
+ constructor(config) {
231
+ this.config = { ...DEFAULT_CASCADE_CONFIG, ...config };
232
+ this.llmLayer = new LlmLayer({
233
+ contextSentences: this.config.contextSentences,
234
+ provider: this.config.llmProvider
235
+ });
236
+ }
237
+ /** Whether the cascade is ready to serve (BERT model loaded if enabled) */
238
+ get ready() {
239
+ if (!this.config.bertEnabled) return true;
240
+ if (!this.bertLayer) return true;
241
+ return this.bertLayer.loaded;
242
+ }
243
+ /** Register regex-based guards (Layer 1) */
244
+ addRegexGuard(guard) {
245
+ this.regexGuards.push(guard);
246
+ return this;
247
+ }
248
+ /** Set the LLM provider for Layer 3 */
249
+ setLlmProvider(provider) {
250
+ this.config.llmProvider = provider;
251
+ this.llmLayer.setProvider(provider);
252
+ }
253
+ /**
254
+ * Run the full cascade: Regex → BERT → LLM
255
+ * Returns a unified GuardResult with all detections carrying provenance.
256
+ */
257
+ async deepScan(text) {
258
+ const regexDetections = await this.runRegexLayer(text);
259
+ if (!this.config.bertEnabled) {
260
+ return this.buildCascadeResult(text, regexDetections);
261
+ }
262
+ const bertDetections = await this.runBertLayer(text);
263
+ const mergedBert = this.deduplicateAgainstRegex(
264
+ bertDetections,
265
+ regexDetections
266
+ );
267
+ const allDetections = [...regexDetections, ...mergedBert];
268
+ const escalated = allDetections.filter((d) => d.disposition === "escalate");
269
+ if (!this.config.llmEnabled || escalated.length === 0 || !this.config.llmProvider) {
270
+ return this.buildCascadeResult(text, allDetections);
271
+ }
272
+ const confirmed = allDetections.filter((d) => d.disposition === "confirmed");
273
+ const resolved = await this.llmLayer.disambiguate(
274
+ escalated,
275
+ text,
276
+ confirmed
277
+ );
278
+ const finalDetections = [
279
+ ...allDetections.filter((d) => d.disposition !== "escalate"),
280
+ ...resolved
281
+ ];
282
+ return this.buildCascadeResult(text, finalDetections);
283
+ }
284
+ /** Run Layer 1 only (for fast auto-scan path) */
285
+ async regexScan(text) {
286
+ const detections = await this.runRegexLayer(text);
287
+ return this.buildCascadeResult(text, detections);
288
+ }
289
+ /** Run Layers 1 + 2 only (no LLM, for "Scan File" command) */
290
+ async modelScan(text) {
291
+ const regexDetections = await this.runRegexLayer(text);
292
+ if (!this.config.bertEnabled) {
293
+ return this.buildCascadeResult(text, regexDetections);
294
+ }
295
+ const bertDetections = await this.runBertLayer(text);
296
+ const mergedBert = this.deduplicateAgainstRegex(
297
+ bertDetections,
298
+ regexDetections
299
+ );
300
+ return this.buildCascadeResult(text, [...regexDetections, ...mergedBert]);
301
+ }
302
+ // --- Private methods ---
303
+ async runRegexLayer(text) {
304
+ const allDetections = [];
305
+ for (const guard of this.regexGuards) {
306
+ const result = await guard.analyze(text);
307
+ allDetections.push(...result.detections);
308
+ }
309
+ return allDetections;
310
+ }
311
+ async runBertLayer(text) {
312
+ if (!this.bertLayer) {
313
+ this.bertLayer = new BertLayer({
314
+ modelId: this.config.modelId,
315
+ escalationThreshold: this.config.escalationThreshold
316
+ });
317
+ }
318
+ return this.bertLayer.analyze(text);
319
+ }
320
+ /** Remove BERT detections that overlap with regex detections */
321
+ deduplicateAgainstRegex(bertDetections, regexDetections) {
322
+ return bertDetections.filter((bert) => {
323
+ return !regexDetections.some(
324
+ (regex) => bert.start < regex.end && bert.end > regex.start
325
+ );
326
+ });
327
+ }
328
+ buildCascadeResult(text, detections) {
329
+ const activeDetections = detections.filter(
330
+ (d) => d.disposition !== "dismissed"
331
+ );
332
+ const passed = activeDetections.length === 0;
333
+ const score = activeDetections.length > 0 ? Math.max(...activeDetections.map((d) => d.score)) : 0;
334
+ const sources = [...new Set(detections.map((d) => d.source))];
335
+ const types = [...new Set(activeDetections.map((d) => d.entityType))];
336
+ return {
337
+ passed,
338
+ reason: passed ? "No issues detected" : `Detected via ${sources.join("+")}: ${types.join(", ")}`,
339
+ guardName: "cascade",
340
+ score,
341
+ detections
342
+ };
343
+ }
344
+ /** Clean up resources */
345
+ async dispose() {
346
+ if (this.bertLayer) {
347
+ await this.bertLayer.dispose();
348
+ this.bertLayer = null;
349
+ }
350
+ }
351
+ };
352
+
353
+ export {
354
+ BertLayer,
355
+ LlmLayer,
356
+ CascadeClassifier
357
+ };
358
+ //# sourceMappingURL=chunk-4KUXRYNS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cascade/bert-layer.ts","../src/cascade/llm-layer.ts","../src/cascade/cascade.ts"],"sourcesContent":["/**\n * Main-thread interface to the BERT worker (Layer 2).\n * Manages the worker lifecycle and maps BERT tokens to Detection objects.\n */\n\nimport { Worker } from \"node:worker_threads\";\nimport { resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport type { Detection, Confidence } from \"../types\";\nimport type { WorkerRequest, WorkerResponse, BertToken } from \"./bert-worker\";\n\nconst DEFAULT_MODEL_ID = \"Xenova/bert-base-NER\";\n\nexport interface BertLayerConfig {\n modelId?: string;\n /** Threshold above which detections are confirmed, below which they escalate */\n escalationThreshold: number;\n}\n\nexport class BertLayer {\n private worker: Worker | null = null;\n private pendingRequests = new Map<\n string,\n { resolve: (tokens: BertToken[]) => void; reject: (err: Error) => void }\n >();\n private requestId = 0;\n private config: BertLayerConfig;\n\n /** Whether the BERT model has been loaded and first inference completed */\n private _loaded = false;\n get loaded(): boolean {\n return this._loaded;\n }\n\n constructor(config?: Partial<BertLayerConfig>) {\n this.config = {\n escalationThreshold: 0.75,\n ...config,\n };\n }\n\n /** Resolve the worker path — supports both compiled .js and source .ts */\n private resolveWorkerPath(): string {\n // Compiled: dist/cascade/bert-worker.js (built as separate tsup entry)\n const compiledPath = resolve(__dirname, \"cascade\", \"bert-worker.js\");\n if (existsSync(compiledPath)) return compiledPath;\n\n // Dev/test mode: use TypeScript source with tsx loader\n const tsPath = resolve(__dirname, \"bert-worker.ts\");\n if (existsSync(tsPath)) return tsPath;\n\n return compiledPath; // Fall back to compiled path (will error if missing)\n }\n\n /** Ensure the worker thread is running */\n private ensureWorker(): Worker {\n if (!this.worker) {\n const workerPath = this.resolveWorkerPath();\n const isTs = workerPath.endsWith(\".ts\");\n\n // In dev/test mode (.ts), use tsx to transpile the worker on the fly\n this.worker = isTs\n ? new Worker(workerPath, {\n execArgv: [\"--require\", \"tsx/cjs\"],\n })\n : new Worker(workerPath);\n\n this.worker.on(\"message\", (msg: WorkerResponse) => {\n const pending = this.pendingRequests.get(msg.id);\n if (!pending) return;\n\n this.pendingRequests.delete(msg.id);\n if (msg.type === \"error\") {\n pending.reject(new Error(msg.error ?? \"Unknown worker error\"));\n } else {\n pending.resolve(msg.tokens ?? []);\n }\n });\n\n this.worker.on(\"error\", (err) => {\n // Reject all pending requests\n for (const [id, pending] of this.pendingRequests) {\n pending.reject(err);\n this.pendingRequests.delete(id);\n }\n });\n }\n return this.worker;\n }\n\n /** Send text to the BERT worker and get raw token results */\n private async analyzeRaw(text: string): Promise<BertToken[]> {\n const worker = this.ensureWorker();\n const id = String(++this.requestId);\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, { resolve, reject });\n worker.postMessage({\n type: \"analyze\",\n id,\n text,\n modelId: this.config.modelId ?? DEFAULT_MODEL_ID,\n } as WorkerRequest);\n });\n }\n\n /**\n * Analyze text and return Detection objects with escalation disposition.\n * Tokens above the escalation threshold are \"confirmed\",\n * tokens below are \"escalate\" (need LLM review).\n */\n async analyze(text: string): Promise<Detection[]> {\n const tokens = await this.analyzeRaw(text);\n this._loaded = true;\n\n return tokens.map((token) => {\n // Strip B-/I- prefix from entity labels (e.g., \"B-PERSON\" → \"PERSON\")\n const entityType = token.entity.replace(/^[BI]-/, \"\");\n const isConfirmed = token.score >= this.config.escalationThreshold;\n\n const confidence: Confidence =\n token.score >= 0.9 ? \"high\" : token.score >= 0.7 ? \"medium\" : \"low\";\n\n return {\n entityType,\n start: token.start,\n end: token.end,\n text: token.word,\n confidence,\n score: token.score,\n guardName: \"cascade-bert\",\n source: \"bert\" as const,\n context: text.slice(\n Math.max(0, token.start - 150),\n Math.min(text.length, token.end + 150)\n ),\n disposition: isConfirmed ? (\"confirmed\" as const) : (\"escalate\" as const),\n };\n });\n }\n\n /** Terminate the worker thread */\n async dispose(): Promise<void> {\n if (this.worker) {\n await this.worker.terminate();\n this.worker = null;\n this.pendingRequests.clear();\n }\n }\n}\n","/**\n * LLM disambiguation layer (Layer 3) of the cascading classifier.\n * Only receives ambiguous spans from Layer 2, along with surrounding context.\n * Makes a focused determination: is this span PII or not?\n */\n\nimport type { Detection, Disposition } from \"../types\";\n\n/** Function signature for an LLM provider */\nexport type LlmProvider = (prompt: string) => Promise<string>;\n\nexport interface LlmLayerConfig {\n /** Number of sentences before/after the span to include as context */\n contextSentences: number;\n /** LLM provider function */\n provider?: LlmProvider;\n}\n\ninterface DisambiguationResult {\n type: string;\n confidence: number;\n}\n\nexport class LlmLayer {\n private config: LlmLayerConfig;\n\n constructor(config?: Partial<LlmLayerConfig>) {\n this.config = {\n contextSentences: 3,\n ...config,\n };\n }\n\n /** Set the LLM provider (can be swapped at runtime) */\n setProvider(provider: LlmProvider): void {\n this.config.provider = provider;\n }\n\n /**\n * Disambiguate escalated detections using an LLM.\n * @param escalated Detections with disposition \"escalate\"\n * @param fullText The full document text\n * @param confirmed Already-confirmed detections (passed as context to help disambiguation)\n */\n async disambiguate(\n escalated: Detection[],\n fullText: string,\n confirmed: Detection[]\n ): Promise<Detection[]> {\n if (!this.config.provider) {\n // No LLM configured — return escalated items as-is\n return escalated;\n }\n\n const results: Detection[] = [];\n\n for (const detection of escalated) {\n const prompt = this.buildPrompt(detection, fullText, confirmed);\n\n try {\n const response = await this.config.provider(prompt);\n const parsed = this.parseResponse(response);\n\n if (parsed && parsed.type !== \"NONE\") {\n results.push({\n ...detection,\n entityType: parsed.type,\n score: parsed.confidence,\n confidence:\n parsed.confidence >= 0.9\n ? \"high\"\n : parsed.confidence >= 0.7\n ? \"medium\"\n : \"low\",\n source: \"llm\",\n disposition: \"confirmed\" as Disposition,\n });\n } else {\n results.push({\n ...detection,\n source: \"llm\",\n disposition: \"dismissed\" as Disposition,\n });\n }\n } catch {\n // LLM call failed — keep as escalated rather than losing data\n results.push(detection);\n }\n }\n\n return results;\n }\n\n /** Build a focused disambiguation prompt */\n private buildPrompt(\n detection: Detection,\n fullText: string,\n confirmed: Detection[]\n ): string {\n const contextWindow = this.extractSentenceContext(\n fullText,\n detection.start,\n detection.end\n );\n\n const confirmedList = confirmed\n .filter((d) => d.disposition === \"confirmed\")\n .map((d) => `${d.text} (${d.entityType})`)\n .slice(0, 10); // Limit to avoid excessive token use\n\n return `You are a PII detection system. Determine if the highlighted span is personally identifiable information.\n\nContext: \"${contextWindow}\"\nSpan: \"${detection.text}\"\nBERT suggested: ${detection.entityType} (confidence: ${detection.score.toFixed(2)})\n${confirmedList.length > 0 ? `Other confirmed entities in document: [${confirmedList.join(\", \")}]` : \"\"}\n\nIs this span PII? If yes, what type? If it's ambiguous (e.g., \"Jordan\" could be a person or country), use the context to decide.\n\nRespond with ONLY a JSON object: { \"type\": \"PERSON\"|\"LOCATION\"|\"ORGANIZATION\"|\"NONE\", \"confidence\": 0.0-1.0 }`;\n }\n\n /** Extract ±N sentences around a span */\n private extractSentenceContext(\n text: string,\n start: number,\n end: number\n ): string {\n const n = this.config.contextSentences;\n\n // Split into sentences (simple heuristic)\n const sentenceBreaks: number[] = [0];\n const sentenceRegex = /[.!?]+\\s+/g;\n let match: RegExpExecArray | null;\n while ((match = sentenceRegex.exec(text)) !== null) {\n sentenceBreaks.push(match.index + match[0].length);\n }\n sentenceBreaks.push(text.length);\n\n // Find which sentence contains the span\n let spanSentenceIdx = 0;\n for (let i = 0; i < sentenceBreaks.length - 1; i++) {\n if (sentenceBreaks[i] <= start && start < sentenceBreaks[i + 1]) {\n spanSentenceIdx = i;\n break;\n }\n }\n\n // Extract ±n sentences\n const contextStart =\n sentenceBreaks[Math.max(0, spanSentenceIdx - n)];\n const contextEnd =\n sentenceBreaks[\n Math.min(sentenceBreaks.length - 1, spanSentenceIdx + n + 1)\n ];\n\n return text.slice(contextStart, contextEnd).trim();\n }\n\n /** Parse the LLM response JSON */\n private parseResponse(response: string): DisambiguationResult | null {\n try {\n // Extract JSON from response (may have surrounding text)\n const jsonMatch = response.match(/\\{[^}]+\\}/);\n if (!jsonMatch) return null;\n\n const parsed = JSON.parse(jsonMatch[0]);\n if (\n typeof parsed.type === \"string\" &&\n typeof parsed.confidence === \"number\"\n ) {\n return parsed as DisambiguationResult;\n }\n return null;\n } catch {\n return null;\n }\n }\n}\n","/**\n * Cascading Classifier — orchestrates the three detection layers.\n *\n * Layer 1 (Regex): Always runs, sub-ms. Catches structured PII.\n * → confidence: 1.0, disposition: \"confirmed\"\n *\n * Layer 2 (BERT): On-demand, 20-50ms. Catches contextual entities.\n * → score >= threshold: \"confirmed\"\n * → score < threshold: \"escalate\"\n *\n * Layer 3 (LLM): Selective, 500ms-2s. Only sees escalated spans.\n * → Returns \"confirmed\" or \"dismissed\"\n */\n\nimport type { Detection, GuardResult } from \"../types\";\nimport type { Guard } from \"../types\";\nimport { BertLayer, type BertLayerConfig } from \"./bert-layer\";\nimport { LlmLayer, type LlmProvider, type LlmLayerConfig } from \"./llm-layer\";\n\nexport interface CascadeConfig {\n /** Confidence threshold below which BERT results escalate to LLM */\n escalationThreshold: number;\n /** Number of sentences of context to pass to Layer 3 */\n contextSentences: number;\n /** Whether Layer 2 (BERT) is enabled */\n bertEnabled: boolean;\n /** Whether Layer 3 (LLM) is enabled */\n llmEnabled: boolean;\n /** Model ID for BERT layer */\n modelId?: string;\n /** LLM provider function for Layer 3 */\n llmProvider?: LlmProvider;\n}\n\nconst DEFAULT_CASCADE_CONFIG: CascadeConfig = {\n escalationThreshold: 0.75,\n contextSentences: 3,\n bertEnabled: true,\n llmEnabled: false,\n modelId: \"Xenova/bert-base-NER\",\n};\n\nexport class CascadeClassifier {\n private config: CascadeConfig;\n private bertLayer: BertLayer | null = null;\n private llmLayer: LlmLayer;\n private regexGuards: Guard[] = [];\n\n constructor(config?: Partial<CascadeConfig>) {\n this.config = { ...DEFAULT_CASCADE_CONFIG, ...config };\n this.llmLayer = new LlmLayer({\n contextSentences: this.config.contextSentences,\n provider: this.config.llmProvider,\n });\n }\n\n /** Whether the cascade is ready to serve (BERT model loaded if enabled) */\n get ready(): boolean {\n if (!this.config.bertEnabled) return true;\n if (!this.bertLayer) return true; // Not yet initialized, will lazy-load\n return this.bertLayer.loaded;\n }\n\n /** Register regex-based guards (Layer 1) */\n addRegexGuard(guard: Guard): this {\n this.regexGuards.push(guard);\n return this;\n }\n\n /** Set the LLM provider for Layer 3 */\n setLlmProvider(provider: LlmProvider): void {\n this.config.llmProvider = provider;\n this.llmLayer.setProvider(provider);\n }\n\n /**\n * Run the full cascade: Regex → BERT → LLM\n * Returns a unified GuardResult with all detections carrying provenance.\n */\n async deepScan(text: string): Promise<GuardResult> {\n // === Layer 1: Regex (always) ===\n const regexDetections = await this.runRegexLayer(text);\n\n // If BERT is disabled, return regex-only results\n if (!this.config.bertEnabled) {\n return this.buildCascadeResult(text, regexDetections);\n }\n\n // === Layer 2: BERT ===\n const bertDetections = await this.runBertLayer(text);\n\n // Deduplicate: regex wins for overlapping spans\n const mergedBert = this.deduplicateAgainstRegex(\n bertDetections,\n regexDetections\n );\n\n const allDetections = [...regexDetections, ...mergedBert];\n\n // If LLM is disabled or no escalated items, return here\n const escalated = allDetections.filter((d) => d.disposition === \"escalate\");\n if (!this.config.llmEnabled || escalated.length === 0 || !this.config.llmProvider) {\n return this.buildCascadeResult(text, allDetections);\n }\n\n // === Layer 3: LLM (only escalated spans) ===\n const confirmed = allDetections.filter((d) => d.disposition === \"confirmed\");\n const resolved = await this.llmLayer.disambiguate(\n escalated,\n text,\n confirmed\n );\n\n // Replace escalated detections with resolved ones\n const finalDetections = [\n ...allDetections.filter((d) => d.disposition !== \"escalate\"),\n ...resolved,\n ];\n\n return this.buildCascadeResult(text, finalDetections);\n }\n\n /** Run Layer 1 only (for fast auto-scan path) */\n async regexScan(text: string): Promise<GuardResult> {\n const detections = await this.runRegexLayer(text);\n return this.buildCascadeResult(text, detections);\n }\n\n /** Run Layers 1 + 2 only (no LLM, for \"Scan File\" command) */\n async modelScan(text: string): Promise<GuardResult> {\n const regexDetections = await this.runRegexLayer(text);\n if (!this.config.bertEnabled) {\n return this.buildCascadeResult(text, regexDetections);\n }\n\n const bertDetections = await this.runBertLayer(text);\n const mergedBert = this.deduplicateAgainstRegex(\n bertDetections,\n regexDetections\n );\n\n return this.buildCascadeResult(text, [...regexDetections, ...mergedBert]);\n }\n\n // --- Private methods ---\n\n private async runRegexLayer(text: string): Promise<Detection[]> {\n const allDetections: Detection[] = [];\n for (const guard of this.regexGuards) {\n const result = await guard.analyze(text);\n allDetections.push(...result.detections);\n }\n return allDetections;\n }\n\n private async runBertLayer(text: string): Promise<Detection[]> {\n if (!this.bertLayer) {\n this.bertLayer = new BertLayer({\n modelId: this.config.modelId,\n escalationThreshold: this.config.escalationThreshold,\n });\n }\n return this.bertLayer.analyze(text);\n }\n\n /** Remove BERT detections that overlap with regex detections */\n private deduplicateAgainstRegex(\n bertDetections: Detection[],\n regexDetections: Detection[]\n ): Detection[] {\n return bertDetections.filter((bert) => {\n return !regexDetections.some(\n (regex) => bert.start < regex.end && bert.end > regex.start\n );\n });\n }\n\n private buildCascadeResult(\n text: string,\n detections: Detection[]\n ): GuardResult {\n // Only count non-dismissed detections as failures\n const activeDetections = detections.filter(\n (d) => d.disposition !== \"dismissed\"\n );\n const passed = activeDetections.length === 0;\n const score =\n activeDetections.length > 0\n ? Math.max(...activeDetections.map((d) => d.score))\n : 0;\n\n const sources = [...new Set(detections.map((d) => d.source))];\n const types = [...new Set(activeDetections.map((d) => d.entityType))];\n\n return {\n passed,\n reason: passed\n ? \"No issues detected\"\n : `Detected via ${sources.join(\"+\")}: ${types.join(\", \")}`,\n guardName: \"cascade\",\n score,\n detections,\n };\n }\n\n /** Clean up resources */\n async dispose(): Promise<void> {\n if (this.bertLayer) {\n await this.bertLayer.dispose();\n this.bertLayer = null;\n }\n }\n}\n"],"mappings":";AAKA,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAI3B,IAAM,mBAAmB;AAQlB,IAAM,YAAN,MAAgB;AAAA,EACb,SAAwB;AAAA,EACxB,kBAAkB,oBAAI,IAG5B;AAAA,EACM,YAAY;AAAA,EACZ;AAAA;AAAA,EAGA,UAAU;AAAA,EAClB,IAAI,SAAkB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAY,QAAmC;AAC7C,SAAK,SAAS;AAAA,MACZ,qBAAqB;AAAA,MACrB,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA,EAGQ,oBAA4B;AAElC,UAAM,eAAe,QAAQ,WAAW,WAAW,gBAAgB;AACnE,QAAI,WAAW,YAAY,EAAG,QAAO;AAGrC,UAAM,SAAS,QAAQ,WAAW,gBAAgB;AAClD,QAAI,WAAW,MAAM,EAAG,QAAO;AAE/B,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,eAAuB;AAC7B,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,aAAa,KAAK,kBAAkB;AAC1C,YAAM,OAAO,WAAW,SAAS,KAAK;AAGtC,WAAK,SAAS,OACV,IAAI,OAAO,YAAY;AAAA,QACrB,UAAU,CAAC,aAAa,SAAS;AAAA,MACnC,CAAC,IACD,IAAI,OAAO,UAAU;AAEzB,WAAK,OAAO,GAAG,WAAW,CAAC,QAAwB;AACjD,cAAM,UAAU,KAAK,gBAAgB,IAAI,IAAI,EAAE;AAC/C,YAAI,CAAC,QAAS;AAEd,aAAK,gBAAgB,OAAO,IAAI,EAAE;AAClC,YAAI,IAAI,SAAS,SAAS;AACxB,kBAAQ,OAAO,IAAI,MAAM,IAAI,SAAS,sBAAsB,CAAC;AAAA,QAC/D,OAAO;AACL,kBAAQ,QAAQ,IAAI,UAAU,CAAC,CAAC;AAAA,QAClC;AAAA,MACF,CAAC;AAED,WAAK,OAAO,GAAG,SAAS,CAAC,QAAQ;AAE/B,mBAAW,CAAC,IAAI,OAAO,KAAK,KAAK,iBAAiB;AAChD,kBAAQ,OAAO,GAAG;AAClB,eAAK,gBAAgB,OAAO,EAAE;AAAA,QAChC;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAc,WAAW,MAAoC;AAC3D,UAAM,SAAS,KAAK,aAAa;AACjC,UAAM,KAAK,OAAO,EAAE,KAAK,SAAS;AAElC,WAAO,IAAI,QAAQ,CAACA,UAAS,WAAW;AACtC,WAAK,gBAAgB,IAAI,IAAI,EAAE,SAAAA,UAAS,OAAO,CAAC;AAChD,aAAO,YAAY;AAAA,QACjB,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,SAAS,KAAK,OAAO,WAAW;AAAA,MAClC,CAAkB;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAQ,MAAoC;AAChD,UAAM,SAAS,MAAM,KAAK,WAAW,IAAI;AACzC,SAAK,UAAU;AAEf,WAAO,OAAO,IAAI,CAAC,UAAU;AAE3B,YAAM,aAAa,MAAM,OAAO,QAAQ,UAAU,EAAE;AACpD,YAAM,cAAc,MAAM,SAAS,KAAK,OAAO;AAE/C,YAAM,aACJ,MAAM,SAAS,MAAM,SAAS,MAAM,SAAS,MAAM,WAAW;AAEhE,aAAO;AAAA,QACL;AAAA,QACA,OAAO,MAAM;AAAA,QACb,KAAK,MAAM;AAAA,QACX,MAAM,MAAM;AAAA,QACZ;AAAA,QACA,OAAO,MAAM;AAAA,QACb,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,SAAS,KAAK;AAAA,UACZ,KAAK,IAAI,GAAG,MAAM,QAAQ,GAAG;AAAA,UAC7B,KAAK,IAAI,KAAK,QAAQ,MAAM,MAAM,GAAG;AAAA,QACvC;AAAA,QACA,aAAa,cAAe,cAAyB;AAAA,MACvD;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,QAAI,KAAK,QAAQ;AACf,YAAM,KAAK,OAAO,UAAU;AAC5B,WAAK,SAAS;AACd,WAAK,gBAAgB,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;;;AC9HO,IAAM,WAAN,MAAe;AAAA,EACZ;AAAA,EAER,YAAY,QAAkC;AAC5C,SAAK,SAAS;AAAA,MACZ,kBAAkB;AAAA,MAClB,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA,EAGA,YAAY,UAA6B;AACvC,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aACJ,WACA,UACA,WACsB;AACtB,QAAI,CAAC,KAAK,OAAO,UAAU;AAEzB,aAAO;AAAA,IACT;AAEA,UAAM,UAAuB,CAAC;AAE9B,eAAW,aAAa,WAAW;AACjC,YAAM,SAAS,KAAK,YAAY,WAAW,UAAU,SAAS;AAE9D,UAAI;AACF,cAAM,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM;AAClD,cAAM,SAAS,KAAK,cAAc,QAAQ;AAE1C,YAAI,UAAU,OAAO,SAAS,QAAQ;AACpC,kBAAQ,KAAK;AAAA,YACX,GAAG;AAAA,YACH,YAAY,OAAO;AAAA,YACnB,OAAO,OAAO;AAAA,YACd,YACE,OAAO,cAAc,MACjB,SACA,OAAO,cAAc,MACnB,WACA;AAAA,YACR,QAAQ;AAAA,YACR,aAAa;AAAA,UACf,CAAC;AAAA,QACH,OAAO;AACL,kBAAQ,KAAK;AAAA,YACX,GAAG;AAAA,YACH,QAAQ;AAAA,YACR,aAAa;AAAA,UACf,CAAC;AAAA,QACH;AAAA,MACF,QAAQ;AAEN,gBAAQ,KAAK,SAAS;AAAA,MACxB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,YACN,WACA,UACA,WACQ;AACR,UAAM,gBAAgB,KAAK;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAEA,UAAM,gBAAgB,UACnB,OAAO,CAAC,MAAM,EAAE,gBAAgB,WAAW,EAC3C,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,UAAU,GAAG,EACxC,MAAM,GAAG,EAAE;AAEd,WAAO;AAAA;AAAA,YAEC,aAAa;AAAA,SAChB,UAAU,IAAI;AAAA,kBACL,UAAU,UAAU,iBAAiB,UAAU,MAAM,QAAQ,CAAC,CAAC;AAAA,EAC/E,cAAc,SAAS,IAAI,0CAA0C,cAAc,KAAK,IAAI,CAAC,MAAM,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,EAKrG;AAAA;AAAA,EAGQ,uBACN,MACA,OACA,KACQ;AACR,UAAM,IAAI,KAAK,OAAO;AAGtB,UAAM,iBAA2B,CAAC,CAAC;AACnC,UAAM,gBAAgB;AACtB,QAAI;AACJ,YAAQ,QAAQ,cAAc,KAAK,IAAI,OAAO,MAAM;AAClD,qBAAe,KAAK,MAAM,QAAQ,MAAM,CAAC,EAAE,MAAM;AAAA,IACnD;AACA,mBAAe,KAAK,KAAK,MAAM;AAG/B,QAAI,kBAAkB;AACtB,aAAS,IAAI,GAAG,IAAI,eAAe,SAAS,GAAG,KAAK;AAClD,UAAI,eAAe,CAAC,KAAK,SAAS,QAAQ,eAAe,IAAI,CAAC,GAAG;AAC/D,0BAAkB;AAClB;AAAA,MACF;AAAA,IACF;AAGA,UAAM,eACJ,eAAe,KAAK,IAAI,GAAG,kBAAkB,CAAC,CAAC;AACjD,UAAM,aACJ,eACE,KAAK,IAAI,eAAe,SAAS,GAAG,kBAAkB,IAAI,CAAC,CAC7D;AAEF,WAAO,KAAK,MAAM,cAAc,UAAU,EAAE,KAAK;AAAA,EACnD;AAAA;AAAA,EAGQ,cAAc,UAA+C;AACnE,QAAI;AAEF,YAAM,YAAY,SAAS,MAAM,WAAW;AAC5C,UAAI,CAAC,UAAW,QAAO;AAEvB,YAAM,SAAS,KAAK,MAAM,UAAU,CAAC,CAAC;AACtC,UACE,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,eAAe,UAC7B;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AChJA,IAAM,yBAAwC;AAAA,EAC5C,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,SAAS;AACX;AAEO,IAAM,oBAAN,MAAwB;AAAA,EACrB;AAAA,EACA,YAA8B;AAAA,EAC9B;AAAA,EACA,cAAuB,CAAC;AAAA,EAEhC,YAAY,QAAiC;AAC3C,SAAK,SAAS,EAAE,GAAG,wBAAwB,GAAG,OAAO;AACrD,SAAK,WAAW,IAAI,SAAS;AAAA,MAC3B,kBAAkB,KAAK,OAAO;AAAA,MAC9B,UAAU,KAAK,OAAO;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,IAAI,QAAiB;AACnB,QAAI,CAAC,KAAK,OAAO,YAAa,QAAO;AACrC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA,EAGA,cAAc,OAAoB;AAChC,SAAK,YAAY,KAAK,KAAK;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,eAAe,UAA6B;AAC1C,SAAK,OAAO,cAAc;AAC1B,SAAK,SAAS,YAAY,QAAQ;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,MAAoC;AAEjD,UAAM,kBAAkB,MAAM,KAAK,cAAc,IAAI;AAGrD,QAAI,CAAC,KAAK,OAAO,aAAa;AAC5B,aAAO,KAAK,mBAAmB,MAAM,eAAe;AAAA,IACtD;AAGA,UAAM,iBAAiB,MAAM,KAAK,aAAa,IAAI;AAGnD,UAAM,aAAa,KAAK;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,gBAAgB,CAAC,GAAG,iBAAiB,GAAG,UAAU;AAGxD,UAAM,YAAY,cAAc,OAAO,CAAC,MAAM,EAAE,gBAAgB,UAAU;AAC1E,QAAI,CAAC,KAAK,OAAO,cAAc,UAAU,WAAW,KAAK,CAAC,KAAK,OAAO,aAAa;AACjF,aAAO,KAAK,mBAAmB,MAAM,aAAa;AAAA,IACpD;AAGA,UAAM,YAAY,cAAc,OAAO,CAAC,MAAM,EAAE,gBAAgB,WAAW;AAC3E,UAAM,WAAW,MAAM,KAAK,SAAS;AAAA,MACnC;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,UAAM,kBAAkB;AAAA,MACtB,GAAG,cAAc,OAAO,CAAC,MAAM,EAAE,gBAAgB,UAAU;AAAA,MAC3D,GAAG;AAAA,IACL;AAEA,WAAO,KAAK,mBAAmB,MAAM,eAAe;AAAA,EACtD;AAAA;AAAA,EAGA,MAAM,UAAU,MAAoC;AAClD,UAAM,aAAa,MAAM,KAAK,cAAc,IAAI;AAChD,WAAO,KAAK,mBAAmB,MAAM,UAAU;AAAA,EACjD;AAAA;AAAA,EAGA,MAAM,UAAU,MAAoC;AAClD,UAAM,kBAAkB,MAAM,KAAK,cAAc,IAAI;AACrD,QAAI,CAAC,KAAK,OAAO,aAAa;AAC5B,aAAO,KAAK,mBAAmB,MAAM,eAAe;AAAA,IACtD;AAEA,UAAM,iBAAiB,MAAM,KAAK,aAAa,IAAI;AACnD,UAAM,aAAa,KAAK;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAEA,WAAO,KAAK,mBAAmB,MAAM,CAAC,GAAG,iBAAiB,GAAG,UAAU,CAAC;AAAA,EAC1E;AAAA;AAAA,EAIA,MAAc,cAAc,MAAoC;AAC9D,UAAM,gBAA6B,CAAC;AACpC,eAAW,SAAS,KAAK,aAAa;AACpC,YAAM,SAAS,MAAM,MAAM,QAAQ,IAAI;AACvC,oBAAc,KAAK,GAAG,OAAO,UAAU;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,aAAa,MAAoC;AAC7D,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,IAAI,UAAU;AAAA,QAC7B,SAAS,KAAK,OAAO;AAAA,QACrB,qBAAqB,KAAK,OAAO;AAAA,MACnC,CAAC;AAAA,IACH;AACA,WAAO,KAAK,UAAU,QAAQ,IAAI;AAAA,EACpC;AAAA;AAAA,EAGQ,wBACN,gBACA,iBACa;AACb,WAAO,eAAe,OAAO,CAAC,SAAS;AACrC,aAAO,CAAC,gBAAgB;AAAA,QACtB,CAAC,UAAU,KAAK,QAAQ,MAAM,OAAO,KAAK,MAAM,MAAM;AAAA,MACxD;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,mBACN,MACA,YACa;AAEb,UAAM,mBAAmB,WAAW;AAAA,MAClC,CAAC,MAAM,EAAE,gBAAgB;AAAA,IAC3B;AACA,UAAM,SAAS,iBAAiB,WAAW;AAC3C,UAAM,QACJ,iBAAiB,SAAS,IACtB,KAAK,IAAI,GAAG,iBAAiB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAChD;AAEN,UAAM,UAAU,CAAC,GAAG,IAAI,IAAI,WAAW,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC5D,UAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,iBAAiB,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEpE,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,SACJ,uBACA,gBAAgB,QAAQ,KAAK,GAAG,CAAC,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,MAC1D,WAAW;AAAA,MACX;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,QAAI,KAAK,WAAW;AAClB,YAAM,KAAK,UAAU,QAAQ;AAC7B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;","names":["resolve"]}