@untangle-code/embeddings-ollama 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +187 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/backoff.d.ts +43 -0
  6. package/dist/backoff.d.ts.map +1 -0
  7. package/dist/backoff.js +38 -0
  8. package/dist/backoff.js.map +1 -0
  9. package/dist/cache.d.ts +17 -0
  10. package/dist/cache.d.ts.map +1 -0
  11. package/dist/cache.js +17 -0
  12. package/dist/cache.js.map +1 -0
  13. package/dist/concurrency.d.ts +11 -0
  14. package/dist/concurrency.d.ts.map +1 -0
  15. package/dist/concurrency.js +31 -0
  16. package/dist/concurrency.js.map +1 -0
  17. package/dist/endpoint.d.ts +16 -0
  18. package/dist/endpoint.d.ts.map +1 -0
  19. package/dist/endpoint.js +32 -0
  20. package/dist/endpoint.js.map +1 -0
  21. package/dist/http.d.ts +54 -0
  22. package/dist/http.d.ts.map +1 -0
  23. package/dist/http.js +136 -0
  24. package/dist/http.js.map +1 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +18 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/inflight.d.ts +49 -0
  30. package/dist/inflight.d.ts.map +1 -0
  31. package/dist/inflight.js +87 -0
  32. package/dist/inflight.js.map +1 -0
  33. package/dist/ollama-api.d.ts +45 -0
  34. package/dist/ollama-api.d.ts.map +1 -0
  35. package/dist/ollama-api.js +40 -0
  36. package/dist/ollama-api.js.map +1 -0
  37. package/dist/provider.d.ts +111 -0
  38. package/dist/provider.d.ts.map +1 -0
  39. package/dist/provider.js +374 -0
  40. package/dist/provider.js.map +1 -0
  41. package/dist/readiness.d.ts +31 -0
  42. package/dist/readiness.d.ts.map +1 -0
  43. package/dist/readiness.js +105 -0
  44. package/dist/readiness.js.map +1 -0
  45. package/dist/sleep.d.ts +8 -0
  46. package/dist/sleep.d.ts.map +1 -0
  47. package/dist/sleep.js +20 -0
  48. package/dist/sleep.js.map +1 -0
  49. package/dist/truncation.d.ts +30 -0
  50. package/dist/truncation.d.ts.map +1 -0
  51. package/dist/truncation.js +39 -0
  52. package/dist/truncation.js.map +1 -0
  53. package/dist/types.d.ts +108 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +7 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/validation.d.ts +2 -0
  58. package/dist/validation.d.ts.map +1 -0
  59. package/dist/validation.js +16 -0
  60. package/dist/validation.js.map +1 -0
  61. package/package.json +62 -3
  62. package/src/backoff.ts +73 -0
  63. package/src/cache.ts +30 -0
  64. package/src/concurrency.ts +37 -0
  65. package/src/endpoint.ts +31 -0
  66. package/src/http.ts +168 -0
  67. package/src/index.ts +21 -0
  68. package/src/inflight.ts +102 -0
  69. package/src/ollama-api.ts +93 -0
  70. package/src/provider.ts +477 -0
  71. package/src/readiness.ts +203 -0
  72. package/src/sleep.ts +27 -0
  73. package/src/truncation.ts +41 -0
  74. package/src/types.ts +132 -0
  75. package/src/validation.ts +23 -0
package/NOTICE ADDED
@@ -0,0 +1,90 @@
1
+ # NOTICE
2
+
3
+ Copyright (C) 2026 Luke Bölling and contributors.
4
+
5
+ This attribution covers original Untangle work. Contributors retain copyright in
6
+ their contributions. Third-party packages, grammars and other materials retain
7
+ their own copyright holders, licenses and notices; this line does not replace them.
8
+
9
+ ## License — binding decision
10
+
11
+ Untangle is licensed under the **GNU Affero General Public License, Version 3
12
+ (AGPL-3.0-only)**. See [`LICENSE`](./LICENSE).
13
+
14
+ This decision is **final, not provisional**, and was explicitly instructed by
15
+ the project owner. No contributor, agent or tool may change it without a new
16
+ explicit instruction.
17
+
18
+ The repository-wide license covers Untangle's first-party source code, scripts,
19
+ tests, fixtures, configuration, documentation, website and brand artwork, including
20
+ the logo and 3D yarn illustrations. Every workspace `package.json` declares
21
+ `"license": "AGPL-3.0-only"`. There is no separately licensed enterprise component
22
+ or separate permissive license for documentation or brand assets.
23
+
24
+ Third-party code, embedded grammars, dependency assets and their notices retain
25
+ their original licenses. Their inclusion does not relicense those works. The
26
+ repository license does not grant trademark rights or change the license of a
27
+ repository analyzed with Untangle. See the repository's `docs/LICENSING.md` for
28
+ scope and verification details.
29
+
30
+ This resolves the license question left open in `vision.md` §14 and completes
31
+ `product.md` §16, item 2.
32
+
33
+ ## Rationale
34
+
35
+ - **Network copyleft fits the intended product model.** AGPL §13 sets source
36
+ availability requirements for modified versions used through a computer network.
37
+ The current local CLI/stdio-MCP entry and future shared-server modes share the
38
+ same license.
39
+ - **Local-first analysis has a clear boundary.** Running Untangle against a codebase
40
+ does not replace that codebase's own license with Untangle's license.
41
+ - **First-party components share one license.** Implemented components and future
42
+ official plugins use this repository's licensing policy. Planned integrations do
43
+ not establish implemented or published products.
44
+
45
+ ## Deliberately accepted consequence
46
+
47
+ The project deliberately chooses AGPL rather than a permissive embedding license.
48
+ Redistribution, modifications and combined works must be assessed against the
49
+ actual terms in `LICENSE`, including applicable Corresponding Source requirements.
50
+ No permissive exception, commercial dual license or special contract-only package
51
+ license is granted or promised by this notice.
52
+
53
+ ## Dependencies
54
+
55
+ Third-party runtime dependencies are checked against the allowlist in
56
+ `scripts/check-licenses.mjs`. Their licenses and copyright notices remain unchanged
57
+ and traceable through package `THIRD_PARTY_LICENSES.md` files, grammar provenance,
58
+ the lockfile and release SBOM. The allowlist includes MIT / Apache-2.0 / BSD / ISC
59
+ and other explicitly listed licenses; it is not a license grant for dependencies.
60
+
61
+ The allowlist in `scripts/check-licenses.mjs` applies to **dependencies**, not
62
+ workspace packages. Untangle's own packages carry AGPL-3.0-only and are excluded
63
+ from that check.
64
+
65
+ ## Clean room
66
+
67
+ Untangle is an independent implementation with its own data models, APIs, tool
68
+ names, tests and sources. Feature ideas may be derived from publicly observable
69
+ behavior and generally known techniques. Third-party source code is not copied,
70
+ including from AGPL projects even where the license would permit it.
71
+
72
+ ## Contribution terms
73
+
74
+ Pull requests are welcome under the repository's existing AGPL-3.0-only license.
75
+ No separate CLA, copyright assignment or DCO sign-off is required. Contributors
76
+ must have the right to submit their work under those terms. See CONTRIBUTING.md.
77
+
78
+ ## Release decisions
79
+
80
+ - [x] Copyright attribution: Luke Bölling, with contributors retaining their rights.
81
+ - [x] Contributions use AGPL-3.0-only without an additional CLA or DCO gate.
82
+
83
+ ## Open legal items before public beta
84
+
85
+ - [ ] Trademark search for the product name `Untangle`.
86
+ - [ ] Registration and legal review of the npm scope `@untangle-code`.
87
+ - [ ] Verify source availability and notices for each released network daemon/server
88
+ mode (WP-59). The current stdio MCP `untangle_about` already reports the license,
89
+ source repository and build commit when supplied; metadata alone does not prove
90
+ that a concrete deployed version's source is available.
package/README.md CHANGED
@@ -1,3 +1,188 @@
1
- # Temporary Holding Version
1
+ # @untangle-code/embeddings-ollama
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Ollama `EmbeddingProvider` adapter (WP-10). An internal private package, not published to npm.
4
+ It implements domain's `EmbeddingProvider` port against real or managed Ollama: automatic endpoint
5
+ detection, seven-step readiness, connection-limited batching with exponential backoff/jitter,
6
+ a truncation guard and embedding cache.
7
+
8
+ Normative sources: [`spec.md`](../../spec.md) §9 (takes precedence; all of 9.1 readiness,
9
+ 9.2 batching, 9.3 truncation, 9.4 cache), §5.7 (embedding profile ID), §4.5 (configuration),
10
+ §3.1–3.2 (modes/endpoints), and [`architecture.md`](../../architecture.md) §6.8.
11
+
12
+ <a id="was-dieses-package-tut"></a>
13
+
14
+ ## Responsibilities
15
+
16
+ `OllamaEmbeddingProvider` (`src/provider.ts`) is the only exported class implementing
17
+ `EmbeddingProvider`. Its first `ensureReady()`/`embedDocuments()`/`embedQuery()` performs exactly
18
+ one readiness sequence from spec.md §9.1 and caches its result for the instance's lifetime:
19
+
20
+ ```text
21
+ 1. Determine effective endpoint — "auto": native (127.0.0.1:11434) first, otherwise managed (11435)
22
+ 2. Check reachability — GET /api/tags
23
+ 3. Check model availability — tag match (":latest" suffix tolerated)
24
+ 4. Pull in managed mode — POST /api/pull, ONLY for Untangle-managed endpoints
25
+ 5. Generate probe embedding — POST /api/embed with fixed probe text
26
+ 6. Validate vector dimensions — EMBEDDING_DIMENSION_MISMATCH on mismatch
27
+ 7. Cache profile/probe time — EmbeddingReadinessV1, valid for the instance lifetime
28
+ ```
29
+
30
+ A dimension mismatch is therefore detected no later than the first
31
+ `embedDocuments()`/`embedQuery()` call: both invoke `ensureReady()` before any batch is processed
32
+ (spec.md §4.8, before the first index upsert).
33
+
34
+ <a id="modulübersicht"></a>
35
+
36
+ ## Modules
37
+
38
+ | Module | Contents |
39
+ |---|---|
40
+ | `provider.ts` | `OllamaEmbeddingProvider`, the sole `EmbeddingProvider` implementation; readiness cache, batching, cache/retry/truncation orchestration |
41
+ | `readiness.ts` | `runReadinessSequence()`, the seven steps in spec.md §9.1 in exactly that order |
42
+ | `http.ts` | `OllamaHttpClient`, a `fetch` wrapper with timeout, `AbortSignal`, bounded connection semaphore; `OllamaHttpError` (transient/permanent), `OllamaRequestCancelledError` |
43
+ | `ollama-api.ts` | Typed subset of Ollama HTTP API: `/api/tags`, `/api/pull`, `/api/embed` |
44
+ | `backoff.ts` | `withRetry()`, exponential backoff with full jitter through injected `Random` |
45
+ | `concurrency.ts` | `mapWithConcurrency()`, a small counting semaphore for bounded parallelism and preserved output order |
46
+ | `truncation.ts` | `estimateTokenCount()`/`wouldProviderTruncate()`, conservative offline truncation-guard estimates (spec.md §9.3) |
47
+ | `cache.ts` | `InMemoryEmbeddingCache`, default `EmbeddingCachePort` implementation |
48
+ | `endpoint.ts` | `redactEndpointUrl()`/`joinUrl()` |
49
+ | `validation.ts` | `assertVectorDimension()`, shared by readiness probes and regular batches |
50
+ | `sleep.ts` | `realSleep`, the only production `SleepFn` implementation |
51
+ | `types.ts` | `OllamaEmbeddingProviderConfig`, `EmbeddingCachePort`/`EmbeddingCacheKey`/`EmbeddingCacheEntry`, `SleepFn`, `EmbeddingBatchProgressV1` |
52
+
53
+ `src/index.ts` is the only public export (rule 10). `http.ts`, `ollama-api.ts`, `readiness.ts`,
54
+ `backoff.ts`, `concurrency.ts`, `truncation.ts`, `endpoint.ts` and `validation.ts` are internal
55
+ implementation details imported directly only by co-located tests.
56
+
57
+ <a id="verwendung"></a>
58
+
59
+ ## Usage
60
+
61
+ ```ts
62
+ import { OllamaEmbeddingProvider } from "@untangle-code/embeddings-ollama";
63
+
64
+ const provider = new OllamaEmbeddingProvider(
65
+ {
66
+ profile: { provider: "ollama", model: "nomic-embed-text", dimensions: 768, /* … */ },
67
+ mode: "auto",
68
+ contextLength: 2048,
69
+ batchSize: 32,
70
+ maxConnections: 4,
71
+ requestTimeoutMs: 120000,
72
+ },
73
+ { clock, random }, // from @untangle-code/domain; cache/sleep/fetchImpl optional
74
+ );
75
+
76
+ await provider.ensureReady();
77
+ const { vectors, truncated } = await provider.embedDocuments(preparedDocumentTexts);
78
+ ```
79
+
80
+ `profile` contains exactly domain's eight `EmbeddingProfile` fields contributing to
81
+ `embedding_profile_id` (spec.md §5.7); other fields are operational and deliberately NOT part of
82
+ profile identity. The caller, typically config's `deriveEmbeddingProfile`, builds it from user
83
+ configuration. This package has no runtime dependency on `config` (rule 3: no dependency on a
84
+ package being developed in parallel in the same wave).
85
+
86
+ <a id="entscheidungen"></a>
87
+
88
+ ## Decisions
89
+
90
+ Choices left open by spec.md/architecture.md:
91
+
92
+ - **HTTP API:** `/api/embed` (batch-capable, `input: string[]`) rather than old `/api/embeddings`
93
+ (one text per request), for both `embedDocuments()` and `embedQuery()` (a one-entry array),
94
+ keeping exactly one code path.
95
+ - **`/api/pull` with `stream: false`:** Ollama waits for the completed pull and returns one JSON
96
+ response rather than NDJSON, symmetrical with every other client request and requiring no
97
+ streaming parser.
98
+ - **Truncation detection is an offline estimate, not a server signal.** `/api/embed` does not
99
+ report actual truncation per input. The adapter compares a conservative token estimate—more
100
+ likely high than low—to `contextLength`, the same trade-off `@untangle-code/chunker` uses for
101
+ budgeting. It is deliberately duplicated rather than imported: chunker estimates beforehand
102
+ to respect budgets; this package estimates afterward to detect overruns. A runtime dependency
103
+ between them would not be justified.
104
+ - **Endpoint selection in `"auto"`:** native (`http://127.0.0.1:11434` by default) first, then
105
+ managed (`http://127.0.0.1:11435`) as fallback (spec.md §3.1). Only the latter is treated as
106
+ Untangle-managed; model pull (step 4) happens exclusively there, never at native or
107
+ `"external"` endpoints (spec.md §3.2 forbids modifying other processes).
108
+ - **Readiness is cached exactly once per instance** (spec.md §9.1 step 7), without TTL expiration.
109
+ A failed attempt is NOT cached; the next call restarts the sequence. A hard reset, for example
110
+ after an endpoint change, requires a new instance; deliberately simple for the MVP.
111
+ - **Bounded connection pool:** a counting semaphore around each `fetch` (`http.ts`), not an
112
+ implementation setting of `fetch` itself. Node 22's `fetch`/`undici` exposes no directly
113
+ injectable connection limit here.
114
+ - **Batching versus connection pool:** `batchSize` controls prepared texts in ONE `/api/embed`
115
+ request; `maxConnections` controls simultaneous requests (`concurrency.ts`). Progress is
116
+ reported per completed network batch through `onBatchProgress`, an extension of
117
+ `PortCallOptions` because the port itself has no progress callback.
118
+ - **Error classification** (spec.md §9.2): HTTP 429/5xx and network errors are transient/retryable;
119
+ other 4xx responses are permanent and NOT retried. Permanent errors stop the batch explicitly
120
+ with `EMBEDDING_UNAVAILABLE`, `retryable: false`, rather than continuing silently or indefinitely.
121
+ - **Cache is NOT persistent by default.** `InMemoryEmbeddingCache` loses state with the process.
122
+ The narrow `EmbeddingCachePort` (`get`/`set` over `{ embeddingProfileId, preparedTextHash }`)
123
+ allows `storage-local` or another adapter to inject persistence without changing this package.
124
+ - **Cache is read a second time BEFORE requesting** (WP-117). `embedSlice` makes its miss/join
125
+ decision once, from one `Promise.all` over `cache.get()`. A cache whose `get()` observes state at
126
+ invocation but resolves later—any network/disk cache, precisely the extension point above—would
127
+ otherwise open a race: caller A can finish, write the entry and release its in-flight slot
128
+ between B's read invocation and resolution; B would find neither cache hit nor in-flight entry
129
+ and embed the same text again. `fetchAndSettle` therefore rereads cache immediately before
130
+ requesting, strictly AFTER claiming the in-flight slot, which is released only after
131
+ `cache.set()` resolves. Exactly ONE additional read per claimed key: no waiting on another
132
+ caller's deferred work, so no deadlock, and no retry loop. **Cost:** a second `get()` on a real
133
+ miss, far cheaper than duplicate GPU-bound embedding. Read failures propagate rather than being
134
+ swallowed (E4), with the same visible behavior as failure of the first read.
135
+ - **Second-read boundary:** it protects against duplicate work WITHIN one provider instance.
136
+ Caches without read-your-writes guarantees, such as replicated distributed caches, or separate
137
+ process instances can still embed twice; preventing that requires locking in the cache backend,
138
+ not this adapter.
139
+
140
+ <a id="offene-punkte-openissues"></a>
141
+
142
+ ## Open issues (`openIssues`)
143
+
144
+ - **`EmbeddingCachePort` conceptually belongs beside `EmbeddingProvider` in domain's `ports.ts`.**
145
+ It is local here because the original task required a local interface plus an open issue,
146
+ allowing immediate adapter testing without parallel edits to domain in the same wave. A
147
+ follow-up work package should move `EmbeddingCacheKey`/`EmbeddingCacheEntry`/`EmbeddingCachePort`
148
+ to domain and switch this adapter—and `storage-local` once it supplies persistence—to that import.
149
+ - **`SleepFn` is also not a domain port.** Domain has `Clock` to read time, but no scheduling port
150
+ for an abortable wait of N milliseconds. A local type meets this package's determinism needs;
151
+ moving it to domain would be natural if another adapter needs it.
152
+ - **Truncation detection is heuristic, not exact** (see Decisions). Without the model's real
153
+ tokenizer, which Ollama does not expose, exact prediction of server truncation is impossible.
154
+ The estimate is intentionally conservative, preferring excess `embedding_truncated=true` flags
155
+ over missing them, consistent with vision.md §8.4 (no silent data loss).
156
+
157
+ ## Tests
158
+
159
+ - `src/*.test.ts`: network-free tests against injected fake `fetch`
160
+ (`test-helpers.ts`: `createFakeOllamaFetch`). Core coverage runs
161
+ `runEmbeddingProviderContract()` from `@untangle-code/testkit` against `OllamaEmbeddingProvider`
162
+ (`provider.test.ts`), plus dimension mismatch, backoff schedule (`backoff.test.ts`, deterministic
163
+ `Random` and recording `SleepFn`), cache hits without a second request, mid-batch `AbortSignal`
164
+ cancellation and truncation guards.
165
+ - `src/provider.inflight.test.ts`: cache concurrency, in-flight deduplication (WP-78) and the
166
+ late-resolving-cache duplicate-embedding window (WP-117). `GatedLateEmbeddingCache` in
167
+ `test-helpers.ts` can hold a read beyond another call's completion without timing delays.
168
+ - `src/ollama.integration.test.ts`: real Ollama at `127.0.0.1:11434` with `qwen3-embedding:0.6b`
169
+ (1024 dimensions). It self-skips (`describe.skipIf`) when the service/model is unavailable.
170
+ Run separately with `pnpm --filter @untangle-code/embeddings-ollama run test:integration`, never
171
+ as part of `pnpm test`/`vitest run` (see `vitest.config.ts` excludes and `vitest.integration.config.ts`).
172
+
173
+ <a id="lizenz"></a>
174
+
175
+ ## License
176
+
177
+
178
+ **AGPL-3.0-only.** This package includes the complete license in [`LICENSE`](./LICENSE), the
179
+ license-choice rationale in [`NOTICE`](./NOTICE), and licenses for all third-party production
180
+ dependencies in [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md). All three ship in the npm
181
+ tarball: AGPL-3.0-only §4 and §5 require recipients to receive the license; a README alone is not
182
+ a copy of the license.
183
+
184
+ Local use alone does not trigger an obligation. Copyleft obligations apply to redistribution and
185
+ providing a network service (AGPL §13).
186
+
187
+ Corresponding TypeScript source ships under `src/` in the same tarball; the complete repository
188
+ is available at <https://github.com/lboel/untangle>.
@@ -0,0 +1,24 @@
1
+ # Third-party licenses — @untangle-code/embeddings-ollama
2
+
3
+ Generated by `pnpm third-party-licenses` from this package's installed
4
+ production dependency tree: transitive `dependencies`, across
5
+ workspace edges, with no `devDependencies` included. The generator reads
6
+ each resolved package's `package.json` in `node_modules`; nothing is maintained by hand
7
+ or estimated.
8
+
9
+ Do not edit manually: `pnpm third-party-licenses --check` fails on drift,
10
+ and `pnpm check-publishable` rejects packages without a current file.
11
+
12
+ This file exists because `docs/BUILD_PLAN.md`, risk R6, makes
13
+ per-package `THIRD_PARTY_LICENSES.md` a release gate for npm publication.
14
+
15
+ Untangle itself is **AGPL-3.0-only** (see `LICENSE` and `NOTICE` in
16
+ this package). The packages below are third-party works; their own
17
+ licenses and copyright notices continue to apply unchanged. Workspace
18
+ packages (`@untangle-code/*`) are not listed here; they belong to the same
19
+ work and use the same AGPL-3.0-only license.
20
+
21
+ ## Result: no third-party dependencies
22
+
23
+ This package has no production dependencies outside the workspace.
24
+ The list is empty because none exist, not because they were not checked.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Exponential backoff with full jitter (spec.md §9.2).
3
+ *
4
+ * Jitter is drawn from the injected `Random` port, never `Math.random()`
5
+ * (BUILD_PLAN global rule, enforced repo-wide by `eslint.config.js`'s
6
+ * `no-restricted-syntax`). Delays are executed via an injected {@link
7
+ * SleepFn} so tests can assert the retry schedule without a real wait.
8
+ */
9
+ import type { Random } from "@untangle-code/domain";
10
+ import type { SleepFn } from "./types.js";
11
+ export interface BackoffConfig {
12
+ /** Maximum number of retries after the first attempt. */
13
+ maxRetries: number;
14
+ baseDelayMs: number;
15
+ maxDelayMs: number;
16
+ }
17
+ /**
18
+ * "Full jitter" delay for retry attempt `attempt` (0-based, i.e. the first
19
+ * retry after the initial attempt is `attempt === 0`): uniform random in
20
+ * `[0, min(maxDelayMs, baseDelayMs * 2^attempt)]`.
21
+ */
22
+ export declare function computeBackoffDelayMs(attempt: number, config: BackoffConfig, random: Random): number;
23
+ export interface RetryOptions {
24
+ signal?: AbortSignal | undefined;
25
+ /** Whether `error` is worth retrying at all; a `false` result stops immediately. */
26
+ isRetryable: (error: unknown) => boolean;
27
+ }
28
+ /**
29
+ * Runs `fn`, retrying on retryable errors up to `config.maxRetries` times
30
+ * with exponential backoff and jitter (spec.md §9.2). A non-retryable error,
31
+ * or a retryable error that has exhausted its retries, propagates as-is —
32
+ * the caller (the `EmbeddingProvider`) is responsible for turning that into
33
+ * the right `UntangleError` ("repeated permanent errors MUST stop the job in
34
+ * a controlled way", spec.md §9.2).
35
+ *
36
+ * Checks `signal` before every attempt and every sleep so an abort mid-retry
37
+ * surfaces immediately rather than after the next attempt completes.
38
+ */
39
+ export declare function withRetry<T>(fn: (attempt: number) => Promise<T>, config: BackoffConfig, deps: {
40
+ random: Random;
41
+ sleep: SleepFn;
42
+ }, options: RetryOptions): Promise<T>;
43
+ //# sourceMappingURL=backoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,CAGR;AAED,MAAM,WAAW,YAAY;IAG3B,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,oFAAoF;IACpF,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EACnC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EACxC,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,CAAC,CAAC,CAeZ"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * "Full jitter" delay for retry attempt `attempt` (0-based, i.e. the first
3
+ * retry after the initial attempt is `attempt === 0`): uniform random in
4
+ * `[0, min(maxDelayMs, baseDelayMs * 2^attempt)]`.
5
+ */
6
+ export function computeBackoffDelayMs(attempt, config, random) {
7
+ const cap = Math.min(config.maxDelayMs, config.baseDelayMs * 2 ** attempt);
8
+ return Math.round(random.nextFloat() * cap);
9
+ }
10
+ /**
11
+ * Runs `fn`, retrying on retryable errors up to `config.maxRetries` times
12
+ * with exponential backoff and jitter (spec.md §9.2). A non-retryable error,
13
+ * or a retryable error that has exhausted its retries, propagates as-is —
14
+ * the caller (the `EmbeddingProvider`) is responsible for turning that into
15
+ * the right `UntangleError` ("repeated permanent errors MUST stop the job in
16
+ * a controlled way", spec.md §9.2).
17
+ *
18
+ * Checks `signal` before every attempt and every sleep so an abort mid-retry
19
+ * surfaces immediately rather than after the next attempt completes.
20
+ */
21
+ export async function withRetry(fn, config, deps, options) {
22
+ let attempt = 0;
23
+ for (;;) {
24
+ options.signal?.throwIfAborted();
25
+ try {
26
+ return await fn(attempt);
27
+ }
28
+ catch (error) {
29
+ if (attempt >= config.maxRetries || !options.isRetryable(error)) {
30
+ throw error;
31
+ }
32
+ const delayMs = computeBackoffDelayMs(attempt, config, deps.random);
33
+ await deps.sleep(delayMs, options.signal);
34
+ attempt += 1;
35
+ }
36
+ }
37
+ }
38
+ //# sourceMappingURL=backoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAe,EACf,MAAqB,EACrB,MAAc;IAEd,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;IAC3E,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,GAAG,CAAC,CAAC;AAC9C,CAAC;AAUD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,EAAmC,EACnC,MAAqB,EACrB,IAAwC,EACxC,OAAqB;IAErB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,SAAS,CAAC;QACR,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,OAAO,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChE,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACpE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAC1C,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Default in-memory {@link EmbeddingCachePort} (spec.md §9.4).
3
+ *
4
+ * Process-local only — a fresh process starts with an empty cache. This is
5
+ * the right default for a library that must work without any storage
6
+ * adapter configured; `storage-local` (or another persistent adapter) can
7
+ * inject a durable implementation of the same port instead.
8
+ */
9
+ import type { EmbeddingCacheEntry, EmbeddingCacheKey, EmbeddingCachePort } from "./types.js";
10
+ export declare class InMemoryEmbeddingCache implements EmbeddingCachePort {
11
+ private readonly entries;
12
+ get(key: EmbeddingCacheKey): Promise<EmbeddingCacheEntry | null>;
13
+ set(key: EmbeddingCacheKey, entry: EmbeddingCacheEntry): Promise<void>;
14
+ /** Test/debug helper: number of cached entries. */
15
+ get size(): number;
16
+ }
17
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAM7F,qBAAa,sBAAuB,YAAW,kBAAkB;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAErD,GAAG,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAIhE,GAAG,CAAC,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF,mDAAmD;IACnD,IAAW,IAAI,IAAI,MAAM,CAExB;CACF"}
package/dist/cache.js ADDED
@@ -0,0 +1,17 @@
1
+ function cacheKeyString(key) {
2
+ return `${key.embeddingProfileId}:${key.preparedTextHash}`;
3
+ }
4
+ export class InMemoryEmbeddingCache {
5
+ entries = new Map();
6
+ async get(key) {
7
+ return this.entries.get(cacheKeyString(key)) ?? null;
8
+ }
9
+ async set(key, entry) {
10
+ this.entries.set(cacheKeyString(key), entry);
11
+ }
12
+ /** Test/debug helper: number of cached entries. */
13
+ get size() {
14
+ return this.entries.size;
15
+ }
16
+ }
17
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAUA,SAAS,cAAc,CAAC,GAAsB;IAC5C,OAAO,GAAG,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAC,gBAAgB,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,OAAO,sBAAsB;IAChB,OAAO,GAAG,IAAI,GAAG,EAA+B,CAAC;IAE3D,KAAK,CAAC,GAAG,CAAC,GAAsB;QACrC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;IACvD,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,GAAsB,EAAE,KAA0B;QACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED,mDAAmD;IACnD,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bounded-concurrency map (spec.md §9.2 "Requests MÜSSEN nach
3
+ * Verbindungslimit begrenzt werden").
4
+ *
5
+ * A small counting semaphore rather than a dependency on an external queue
6
+ * library — the whole need here is "run at most N of these concurrently,
7
+ * preserve output order", which is a handful of lines.
8
+ */
9
+ /** Runs `fn` over `items` with at most `limit` concurrent calls, preserving output order. */
10
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
11
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAC3C,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC,CAAC,EAAE,CAAC,CAsBd"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Bounded-concurrency map (spec.md §9.2 "Requests MÜSSEN nach
3
+ * Verbindungslimit begrenzt werden").
4
+ *
5
+ * A small counting semaphore rather than a dependency on an external queue
6
+ * library — the whole need here is "run at most N of these concurrently,
7
+ * preserve output order", which is a handful of lines.
8
+ */
9
+ /** Runs `fn` over `items` with at most `limit` concurrent calls, preserving output order. */
10
+ export async function mapWithConcurrency(items, limit, fn) {
11
+ if (items.length === 0) {
12
+ return [];
13
+ }
14
+ const boundedLimit = Math.max(1, Math.min(limit, items.length));
15
+ const results = new Array(items.length);
16
+ let nextIndex = 0;
17
+ async function worker() {
18
+ for (;;) {
19
+ const index = nextIndex;
20
+ nextIndex += 1;
21
+ if (index >= items.length) {
22
+ return;
23
+ }
24
+ const item = items[index];
25
+ results[index] = await fn(item, index);
26
+ }
27
+ }
28
+ await Promise.all(Array.from({ length: boundedLimit }, () => worker()));
29
+ return results;
30
+ }
31
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAmB,EACnB,KAAa,EACb,EAA0C;IAE1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,IAAI,KAAK,CAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,UAAU,MAAM;QACnB,SAAS,CAAC;YACR,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC1B,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAM,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACxE,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Endpoint redaction helper (spec.md §3.2 "Health-Ausgaben MÜSSEN die
3
+ * tatsächlich verwendeten Endpunkte anzeigen, wobei Secrets redigiert
4
+ * werden").
5
+ *
6
+ * `@untangle-code/observability`'s `redact()` redacts object fields by key
7
+ * name; it has no notion of a URL's own embedded-credentials syntax
8
+ * (`https://user:pass@host/...`). This is the narrow, URL-specific
9
+ * complement: it never touches the bearer token sent via the
10
+ * `Authorization` header (that never becomes part of the URL to begin
11
+ * with) — it only strips userinfo that a misconfigured `url` might carry.
12
+ */
13
+ export declare function redactEndpointUrl(rawUrl: string): string;
14
+ /** Joins a base endpoint URL with a request path, tolerating a trailing slash on the base. */
15
+ export declare function joinUrl(baseUrl: string, path: string): string;
16
+ //# sourceMappingURL=endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../src/endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAaxD;AAED,8FAA8F;AAC9F,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Endpoint redaction helper (spec.md §3.2 "Health-Ausgaben MÜSSEN die
3
+ * tatsächlich verwendeten Endpunkte anzeigen, wobei Secrets redigiert
4
+ * werden").
5
+ *
6
+ * `@untangle-code/observability`'s `redact()` redacts object fields by key
7
+ * name; it has no notion of a URL's own embedded-credentials syntax
8
+ * (`https://user:pass@host/...`). This is the narrow, URL-specific
9
+ * complement: it never touches the bearer token sent via the
10
+ * `Authorization` header (that never becomes part of the URL to begin
11
+ * with) — it only strips userinfo that a misconfigured `url` might carry.
12
+ */
13
+ export function redactEndpointUrl(rawUrl) {
14
+ try {
15
+ const url = new URL(rawUrl);
16
+ if (url.username || url.password) {
17
+ url.username = "";
18
+ url.password = "";
19
+ return url.toString();
20
+ }
21
+ return rawUrl;
22
+ }
23
+ catch {
24
+ // Not a parseable absolute URL — nothing structured to redact.
25
+ return rawUrl;
26
+ }
27
+ }
28
+ /** Joins a base endpoint URL with a request path, tolerating a trailing slash on the base. */
29
+ export function joinUrl(baseUrl, path) {
30
+ return new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
31
+ }
32
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../src/endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;YACjC,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAC;YAClB,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;QAC/D,OAAO,MAAM,CAAC;IAChB,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,OAAO,CAAC,OAAe,EAAE,IAAY;IACnD,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;AACnF,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /** Thrown by {@link OllamaHttpClient.requestJson} for any failed request. */
2
+ export declare class OllamaHttpError extends Error {
3
+ readonly status: number | undefined;
4
+ readonly retryable: boolean;
5
+ constructor(message: string, options: {
6
+ status?: number;
7
+ retryable: boolean;
8
+ cause?: unknown;
9
+ });
10
+ }
11
+ /** Thrown when a request is aborted by the CALLER's own signal (never retried). */
12
+ export declare class OllamaRequestCancelledError extends Error {
13
+ constructor(cause?: unknown);
14
+ }
15
+ /**
16
+ * True for anything that represents the CALLER's own cancellation rather
17
+ * than a transport failure: an {@link OllamaRequestCancelledError}, a raw
18
+ * `AbortError` (e.g. from `backoff.ts`'s own `signal.throwIfAborted()`
19
+ * pre-check, which runs before this client gets a chance to wrap it), or
20
+ * simply `signal.aborted` already being true by the time the error is
21
+ * observed. Used everywhere this adapter must tell "the caller cancelled"
22
+ * apart from "the endpoint is unreachable" (rule 9: cancellation surfaces as
23
+ * `JOB_CANCELLED`, never as a swallowed/misclassified failure).
24
+ */
25
+ export declare function isRequestCancellation(error: unknown, signal: AbortSignal | undefined): boolean;
26
+ export interface OllamaHttpClientOptions {
27
+ requestTimeoutMs: number;
28
+ maxConnections: number;
29
+ apiKeyEnv?: string | undefined;
30
+ /** Injectable for tests; defaults to the global `fetch`. */
31
+ fetchImpl?: typeof fetch | undefined;
32
+ }
33
+ export declare class OllamaHttpClient {
34
+ private readonly semaphore;
35
+ private readonly fetchImpl;
36
+ private readonly requestTimeoutMs;
37
+ private readonly apiKeyEnv;
38
+ constructor(options: OllamaHttpClientOptions);
39
+ /**
40
+ * Sends one JSON request and parses a JSON response. Never retries by
41
+ * itself — callers wrap this in `backoff.ts`'s `withRetry`.
42
+ *
43
+ * Throws {@link OllamaRequestCancelledError} when `signal` (the caller's
44
+ * own signal, not the internal timeout) is the reason for the abort, and
45
+ * {@link OllamaHttpError} for every other failure (network error, timeout,
46
+ * non-2xx status), with `retryable` set per spec.md §9.2's transient/
47
+ * permanent split.
48
+ */
49
+ requestJson<T>(baseUrl: string, path: string, init: {
50
+ method: "GET" | "POST";
51
+ body?: unknown;
52
+ }, signal?: AbortSignal): Promise<T>;
53
+ }
54
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAQA,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAgB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,SAAgB,SAAS,EAAE,OAAO,CAAC;gBAGjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAOpE;AAED,mFAAmF;AACnF,qBAAa,2BAA4B,SAAQ,KAAK;gBACjC,KAAK,CAAC,EAAE,OAAO;CAInC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAM9F;AAkCD,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IAGvB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAC;CACtC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;gBAE5B,OAAO,EAAE,uBAAuB;IAOnD;;;;;;;;;OASG;IACU,WAAW,CAAC,CAAC,EACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC;CA+Cd"}