@adhd/sox-embedding-provider 0.1.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,255 @@
1
+ /**
2
+ * Process-wide singleton client for the ONE onnxruntime-native-bearing
3
+ * `worker_threads.Worker` allowed to exist in this process (BL-238/BL-171).
4
+ *
5
+ * This worker (`embedWorker.ts`) hosts the MS-MARCO cross-encoder reranker
6
+ * (`@adhd/sox-hybrid-search`) and the DeBERTa NLI verifier
7
+ * (`@adhd/sox-claim-verification`) — both driven by
8
+ * `@huggingface/transformers`' onnxruntime-node@1.24.3. fastembed embeddings
9
+ * (`fastembed.ts`, onnxruntime-node@1.21.0) are DELIBERATELY routed
10
+ * elsewhere — see `sharedFastembedProcess.ts` / `fastembedProcessHost.ts` —
11
+ * for a second, independent native hazard (see below).
12
+ *
13
+ * ── Root cause #1 (proven via a from-scratch minimal repro, no test
14
+ * harness, no mocks — two independent worker_threads.Worker instances, each
15
+ * performing real ONNX inference, concurrently) ──
16
+ *
17
+ * onnxruntime-node's native N-API addon crashes the ENTIRE process (not
18
+ * just the offending worker) with:
19
+ *
20
+ * FATAL ERROR: HandleScope::HandleScope Entering the V8 API without
21
+ * proper locking in place
22
+ * ... Napi::FunctionReference::New(...)
23
+ * ... OrtValueToNapiValue(Napi::Env, Ort::Value&&)
24
+ * ... InferenceSessionWrap::Run(...)
25
+ *
26
+ * whenever 2+ *separate* `worker_threads.Worker` instances (i.e. 2+ separate
27
+ * V8 isolates) each hold an active onnxruntime-node `InferenceSession` and
28
+ * run inference concurrently. The crash fires from inside a `setImmediate`
29
+ * completion callback (`onnxruntime-node/dist/backend.js`), which strongly
30
+ * suggests the native binding keeps some global/static Napi reference that
31
+ * is not isolate-scoped, so a background completion callback belonging to
32
+ * one isolate's session fires while the wrong isolate (or no isolate/
33
+ * HandleScope at all) is active.
34
+ *
35
+ * This reproduces even with TWO workers using the exact SAME
36
+ * onnxruntime-node version (fastembed's 1.21.0, twice) — so it is NOT an
37
+ * ABI/version-mismatch bug between fastembed's onnxruntime-node@1.21.0 and
38
+ * @huggingface/transformers' onnxruntime-node@1.24.3; it is a genuine
39
+ * thread-safety limitation of the onnxruntime-node native addon itself when
40
+ * 2+ instances are concurrently active in one process, regardless of which
41
+ * package loaded which version.
42
+ *
43
+ * ── Root cause #2 (why fastembed is NOT also hosted in this same worker) ──
44
+ *
45
+ * A single shared worker hosting BOTH onnxruntime-node@1.21.0 (fastembed)
46
+ * AND onnxruntime-node@1.24.3 (transformers) was tried and instrumented:
47
+ * even with the two `init` calls strictly serialized in JS (proven via
48
+ * tracing — the second `init` provably did not begin until the first's
49
+ * promise had fully settled), fastembed's init still deterministically threw
50
+ * `std::bad_alloc` when it ran second. This means the two onnxruntime-node
51
+ * major versions leave native state that JS-level Promise resolution does
52
+ * not observe/synchronize (e.g. lingering background native thread-pool
53
+ * teardown) — a hazard below what JS scheduling can prevent. Only a real OS
54
+ * process boundary is proven safe for mixing the two versions; see
55
+ * `sharedFastembedProcess.ts` for that isolation.
56
+ *
57
+ * ── Fix ──
58
+ *
59
+ * Every rerank/verify ONNX consumer in this codebase — the MS-MARCO
60
+ * cross-encoder reranker (`@adhd/sox-hybrid-search`) and the DeBERTa NLI
61
+ * verifier (`@adhd/sox-claim-verification`) — is routed through exactly ONE
62
+ * lazily created, process-wide `worker_threads.Worker` running
63
+ * `embedWorker.ts`. There is never a second onnxruntime-bearing worker alive
64
+ * in the process, so root cause #1 is structurally impossible. fastembed
65
+ * never shares a thread (or process) with this worker at all, so root cause
66
+ * #2 is structurally impossible too.
67
+ *
68
+ * This is the ONLY place a `new Worker(embedWorker.js)` (or any other
69
+ * onnxruntime-bearing worker) should be constructed anywhere in the
70
+ * `@adhd/sox-embedding-provider` / `@adhd/sox-hybrid-search` /
71
+ * `@adhd/sox-claim-verification` triangle. `CrossEncoderWorker` and
72
+ * `WorkerProxy` delegate their wire traffic to
73
+ * `getSharedOnnxWorker().request(...)` instead of spawning their own
74
+ * `Worker`; `FastembedProvider` delegates to
75
+ * `getSharedFastembedProcess().request(...)` instead (a separate child
76
+ * PROCESS, not this worker).
77
+ *
78
+ * Trade-off (accepted, documented): `@adhd/sox-claim-verification`'s
79
+ * `workerCount` pool option (`ClaimVerifierConfig.workerCount`) previously
80
+ * gave real parallelism by spawning N separate worker threads. Since
81
+ * rerank+verify work in the process now funnels through this single shared
82
+ * worker, a `workerCount > 1` no longer buys extra parallelism (every
83
+ * `WorkerProxy` in the pool proxies to the same underlying worker) — but it
84
+ * remains safe (no crash) and does not regress correctness, only
85
+ * throughput under artificially-forced pool concurrency. This is the
86
+ * correct trade for a HIGH-severity whole-process crash.
87
+ */
88
+ import { Worker } from 'node:worker_threads';
89
+ import { fileURLToPath } from 'node:url';
90
+ import { dirname, join } from 'node:path';
91
+ import { existsSync } from 'node:fs';
92
+ const __dirname = dirname(fileURLToPath(import.meta.url));
93
+ /**
94
+ * Resolve `embedWorker.js` regardless of whether this module is running
95
+ * compiled (`dist/sharedOnnxWorker.js`, sitting next to the compiled
96
+ * `dist/embedWorker.js`) or transformed-in-place from source (vitest runs
97
+ * `.ts` files directly via its SSR transform, so `__dirname` resolves to
98
+ * `src/`, which never contains a compiled `.js`) — mirrors the same
99
+ * `dist`-fallback pattern already proven in `embedWorker.spec.ts`.
100
+ */
101
+ function resolveEmbedWorkerPath() {
102
+ const sibling = join(__dirname, 'embedWorker.js');
103
+ if (existsSync(sibling))
104
+ return sibling;
105
+ const distSibling = join(__dirname, '..', 'dist', 'embedWorker.js');
106
+ if (existsSync(distSibling))
107
+ return distSibling;
108
+ // Last resort: return the original candidate so the resulting error names
109
+ // the path that was actually attempted.
110
+ return sibling;
111
+ }
112
+ export class SharedOnnxWorkerClient {
113
+ worker = null;
114
+ startingPromise = null;
115
+ nextId = 1;
116
+ pending = new Map();
117
+ /** True once the underlying Worker has been created. */
118
+ get started() {
119
+ return this.worker !== null;
120
+ }
121
+ /** Lazily create (exactly once) and return the single shared Worker instance. */
122
+ ensureWorker() {
123
+ if (this.worker)
124
+ return Promise.resolve(this.worker);
125
+ if (this.startingPromise)
126
+ return this.startingPromise;
127
+ this.startingPromise = new Promise((resolveStart) => {
128
+ const workerPath = resolveEmbedWorkerPath();
129
+ const w = new Worker(workerPath);
130
+ w.unref();
131
+ w.on('message', (msg) => {
132
+ const pending = this.pending.get(msg.id);
133
+ if (!pending)
134
+ return;
135
+ this.pending.delete(msg.id);
136
+ if ('error' in msg && typeof msg['error'] === 'string') {
137
+ pending.reject(new Error(msg['error']));
138
+ }
139
+ else {
140
+ pending.resolve(msg);
141
+ }
142
+ });
143
+ w.on('error', (err) => {
144
+ for (const { reject } of this.pending.values())
145
+ reject(err);
146
+ this.pending.clear();
147
+ this.worker = null;
148
+ this.startingPromise = null;
149
+ });
150
+ w.on('exit', (code) => {
151
+ if (code !== 0) {
152
+ const err = new Error(`shared ONNX worker exited with code ${code}`);
153
+ for (const { reject } of this.pending.values())
154
+ reject(err);
155
+ this.pending.clear();
156
+ }
157
+ this.worker = null;
158
+ this.startingPromise = null;
159
+ });
160
+ // Attaching a 'message'/'error'/'exit' listener re-refs a Worker's
161
+ // underlying MessagePort even if `.unref()` already ran once before
162
+ // any listener existed (carried over from the BL-fix already applied
163
+ // to the 3 previous per-consumer worker implementations this module
164
+ // replaces). Re-assert unref now that every listener is attached, so
165
+ // a real process can exit once its own work is done instead of
166
+ // hanging on this worker forever.
167
+ w.unref();
168
+ this.worker = w;
169
+ resolveStart(w);
170
+ });
171
+ return this.startingPromise;
172
+ }
173
+ /**
174
+ * Send a request to the shared worker and await its correlated response.
175
+ * Assigns a globally-unique `id` — the caller must NOT set its own `id`
176
+ * (any `id` field on `payload` is ignored/overwritten).
177
+ *
178
+ * Resolves with the raw response message (everything embedWorker.ts sent
179
+ * back except the correlation `id` is semantically meaningful to the
180
+ * caller — e.g. `{ initOk, dim }`, `{ embedding }`, `{ scores }`,
181
+ * `{ result }`). Rejects if the response carries an `error` string, if
182
+ * the shared worker itself errors/exits non-zero while the request is
183
+ * pending, or if `timeoutMs` elapses first.
184
+ */
185
+ async request(payload, timeoutMs) {
186
+ const worker = await this.ensureWorker();
187
+ const id = this.nextId++;
188
+ return new Promise((resolve, reject) => {
189
+ let to;
190
+ if (timeoutMs && timeoutMs > 0) {
191
+ to = setTimeout(() => {
192
+ this.pending.delete(id);
193
+ reject(new Error(`shared ONNX worker request timed out after ${timeoutMs}ms`));
194
+ }, timeoutMs);
195
+ if (typeof to.unref === 'function')
196
+ to.unref();
197
+ }
198
+ this.pending.set(id, {
199
+ resolve: (v) => {
200
+ if (to)
201
+ clearTimeout(to);
202
+ resolve(v);
203
+ },
204
+ reject: (e) => {
205
+ if (to)
206
+ clearTimeout(to);
207
+ reject(e);
208
+ },
209
+ });
210
+ worker.postMessage({ ...payload, id });
211
+ });
212
+ }
213
+ /**
214
+ * Forcefully terminate the shared worker. Intended ONLY for full process
215
+ * shutdown or test teardown that genuinely owns the whole process's ONNX
216
+ * lifecycle — an individual consumer's dispose()/stop() must NOT call
217
+ * this, since other consumers (embed / rerank / verify) may still depend
218
+ * on the shared worker.
219
+ */
220
+ async terminate() {
221
+ const w = this.worker;
222
+ this.worker = null;
223
+ this.startingPromise = null;
224
+ for (const { reject } of this.pending.values()) {
225
+ reject(new Error('shared ONNX worker terminated'));
226
+ }
227
+ this.pending.clear();
228
+ if (w)
229
+ await w.terminate();
230
+ }
231
+ }
232
+ let _singleton = null;
233
+ /**
234
+ * Process-wide singleton accessor — the ONLY sanctioned place a
235
+ * `new Worker(embedWorker.js)` is constructed anywhere in the embed/rerank/
236
+ * verify triangle (BL-238/BL-171). Every ONNX consumer (fastembed
237
+ * embeddings, cross-encoder rerank, NLI verify) must obtain its worker
238
+ * handle through this function instead of constructing its own `Worker`.
239
+ */
240
+ export function getSharedOnnxWorker() {
241
+ if (!_singleton)
242
+ _singleton = new SharedOnnxWorkerClient();
243
+ return _singleton;
244
+ }
245
+ /**
246
+ * Test-only: reset the module-level singleton so a test can exercise a
247
+ * fresh shared-worker lifecycle (e.g. after deliberately crashing/
248
+ * terminating it). Does NOT terminate any existing worker itself — call
249
+ * `.terminate()` on the previous instance first if a clean shutdown is
250
+ * needed.
251
+ */
252
+ export function __resetSharedOnnxWorkerForTests() {
253
+ _singleton = null;
254
+ }
255
+ //# sourceMappingURL=sharedOnnxWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedOnnxWorker.js","sourceRoot":"","sources":["../src/sharedOnnxWorker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,SAAS,sBAAsB;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAClD,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAExC,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACpE,IAAI,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IAEhD,0EAA0E;IAC1E,wCAAwC;IACxC,OAAO,OAAO,CAAC;AACjB,CAAC;AAgBD,MAAM,OAAO,sBAAsB;IACzB,MAAM,GAAkB,IAAI,CAAC;IAC7B,eAAe,GAA2B,IAAI,CAAC;IAC/C,MAAM,GAAG,CAAC,CAAC;IACX,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAElD,wDAAwD;IACxD,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;IAC9B,CAAC;IAED,iFAAiF;IACzE,YAAY;QAClB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrD,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC,eAAe,CAAC;QAEtD,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAS,CAAC,YAAY,EAAE,EAAE;YAC1D,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;YACjC,CAAC,CAAC,KAAK,EAAE,CAAC;YAEV,CAAC,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAkB,EAAE,EAAE;gBACrC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACzC,IAAI,CAAC,OAAO;oBAAE,OAAO;gBACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC5B,IAAI,OAAO,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;oBACvD,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAW,CAAC,CAAC,CAAC;gBACpD,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;gBAC3B,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC5D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;gBACnB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;YAC9B,CAAC,CAAC,CAAC;YAEH,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;gBAC5B,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,uCAAuC,IAAI,EAAE,CAAC,CAAC;oBACrE,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;wBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;oBAC5D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACvB,CAAC;gBACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;gBACnB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;YAC9B,CAAC,CAAC,CAAC;YAEH,mEAAmE;YACnE,oEAAoE;YACpE,qEAAqE;YACrE,oEAAoE;YACpE,qEAAqE;YACrE,+DAA+D;YAC/D,kCAAkC;YAClC,CAAC,CAAC,KAAK,EAAE,CAAC;YAEV,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAChB,YAAY,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,OAAO,CACX,OAAgC,EAChC,SAAkB;QAElB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QACzC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAEzB,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,IAAI,EAA8B,CAAC;YACnC,IAAI,SAAS,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAC/B,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE;oBACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;oBACxB,MAAM,CAAC,IAAI,KAAK,CAAC,8CAA8C,SAAS,IAAI,CAAC,CAAC,CAAC;gBACjF,CAAC,EAAE,SAAS,CAAC,CAAC;gBACd,IAAI,OAAO,EAAE,CAAC,KAAK,KAAK,UAAU;oBAAE,EAAE,CAAC,KAAK,EAAE,CAAC;YACjD,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;gBACnB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;oBACb,IAAI,EAAE;wBAAE,YAAY,CAAC,EAAE,CAAC,CAAC;oBACzB,OAAO,CAAC,CAAM,CAAC,CAAC;gBAClB,CAAC;gBACD,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;oBACZ,IAAI,EAAE;wBAAE,YAAY,CAAC,EAAE,CAAC,CAAC;oBACzB,MAAM,CAAC,CAAC,CAAC,CAAC;gBACZ,CAAC;aACF,CAAC,CAAC;YAEH,MAAM,CAAC,WAAW,CAAC,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAS;QACb,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC;YAAE,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC;IAC7B,CAAC;CACF;AAED,IAAI,UAAU,GAAkC,IAAI,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,UAAU;QAAE,UAAU,GAAG,IAAI,sBAAsB,EAAE,CAAC;IAC3D,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,+BAA+B;IAC7C,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@adhd/sox-embedding-provider",
3
+ "version": "0.1.0",
4
+ "description": "Pluggable text→vector embedding provider — generic EmbeddingProvider interface, config-driven model resolution, async batch-first API (AsyncIterable). Default: fastembed (local ONNX, >=3 model dims proven). Loud-fail: createEmbeddingProvider() throws ResolutionError if config is invalid or model cannot load — no silent downgrade.",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "dependencies": {
26
+ "@huggingface/transformers": "^4.2.0",
27
+ "fastembed": "^2.1.0"
28
+ },
29
+ "sox": {
30
+ "area": "data",
31
+ "group": "embed",
32
+ "concerns": [
33
+ "text→vector (EmbeddingProvider interface)",
34
+ "config-driven model resolution (createEmbeddingProvider factory)",
35
+ "async batch embed (AsyncIterable<Float32Array>)",
36
+ "EmbedRole param (document | query) accepted on embedSingle/embedBatch for interface compatibility — currently ignored (not yet applied) by the fastembed provider",
37
+ "loud-fail ResolutionError at factory time (never mid-call)",
38
+ "three-tier error taxonomy (Transient / Permanent / Resolution)"
39
+ ],
40
+ "invariants": [
41
+ "createEmbeddingProvider() THROWS ResolutionError synchronously or as a rejection if the config is invalid or the model/runtime cannot load — never silently downgrades",
42
+ "every provider advertises { modelId, dimensions, isRemote, isDeterministic, providerUri? } via metadata — callers never hardcode dims",
43
+ "embedBatch() returns AsyncIterable<Float32Array> — callers receive first result before last batch finishes (critical for sequential local inference)",
44
+ "warmUp() is a no-op when isDeterministic === false — currently ALWAYS true, since every shipped provider (fastembed, remote) hard-codes isDeterministic: false, so warmUp() is a no-op on every path today",
45
+ "TransientEmbeddingError → caller may retry; PermanentEmbeddingError → caller must not retry; ResolutionError → factory-time only, never thrown mid-call"
46
+ ],
47
+ "entrypoints": [
48
+ "dist/index.js"
49
+ ]
50
+ }
51
+ }