@dudousxd/nestjs-agent-rag 0.12.0 → 0.13.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.
- package/NOTICE +31 -0
- package/README.md +102 -1
- package/dist/index.cjs +2411 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +807 -5
- package/dist/index.d.ts +807 -5
- package/dist/index.js +2390 -65
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/chunk.ts","../src/vector-store.ts","../src/metadata-patch.ts","../src/filter.ts","../src/memory-vector-store.ts","../src/lexical-query.ts","../src/pg-client.ts","../src/pg-vector-store.ts","../src/redis-vector-store.ts","../src/retrieval-descriptor.ts","../src/embedding-retriever.ts","../src/keyword-retriever.ts","../src/lexical-retriever.ts","../src/hybrid-retriever.ts","../src/reranking-retriever.ts","../src/filtered-retriever.ts","../src/ingest.ts","../src/retrieval-telemetry.ts","../src/retrieval-tool.ts","../src/openai-embeddings.ts","../src/http-reranker.ts"],"sourcesContent":["export { chunkText, type ChunkOptions } from './chunk.js';\nexport type {\n IndexedDocument,\n LexicalVectorStore,\n ListChunksOptions,\n StoredChunk,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nexport {\n UnsafeRemovalError,\n chunkIndexOf,\n documentIdOf,\n isLexicalVectorStore,\n} from './vector-store.js';\nexport type { MetadataPatch } from './metadata-patch.js';\nexport { applyMetadataPatch } from './metadata-patch.js';\nexport { MemoryVectorStore } from './memory-vector-store.js';\nexport {\n DEFAULT_STOP_WORDS,\n anyTermTsquery,\n hasSearchSyntax,\n keywordTerms,\n} from './lexical-query.js';\nexport {\n PgLexicalVectorStore,\n PgVectorStore,\n type PgFullTextOptions,\n type PgVectorStoreOptions,\n} from './pg-vector-store.js';\nexport {\n toPgClient,\n type DrizzleLike,\n type NodePgQueryable,\n type PgClient,\n type PgClientSource,\n type PostgresJsSql,\n} from './pg-client.js';\nexport {\n RedisVectorStore,\n RedisVectorSchemaMismatchError,\n type RedisSearchClient,\n type RedisVectorStoreOptions,\n} from './redis-vector-store.js';\nexport {\n EmbeddingRetriever,\n type EmbeddingRetrieverOptions,\n} from './embedding-retriever.js';\nexport { KeywordRetriever, type KeywordRetrieverOptions } from './keyword-retriever.js';\nexport { LexicalRetriever } from './lexical-retriever.js';\nexport { HybridRetriever, type HybridRetrieverOptions } from './hybrid-retriever.js';\nexport { RerankingRetriever, type RerankingRetrieverOptions } from './reranking-retriever.js';\nexport { FilteredRetriever } from './filtered-retriever.js';\nexport {\n chunkDocuments,\n ingestChunks,\n ingestDocuments,\n type ChunkRecord,\n type IngestChunksOptions,\n type IngestDocument,\n type IngestOptions,\n} from './ingest.js';\nexport {\n createRetrievalTool,\n type RetrievalTool,\n type RetrievalToolOptions,\n} from './retrieval-tool.js';\nexport {\n describeRetrieval,\n describeSharedSource,\n describeSource,\n describesRetrieval,\n type DescribesRetrieval,\n type RetrievalDescriptor,\n type RetrieverKind,\n type VectorStoreKind,\n} from './retrieval-descriptor.js';\nexport {\n emitRagRetrieval,\n instrumentRetriever,\n RAG_DIAGNOSTIC_LIB,\n RAG_RETRIEVAL_CHANNEL,\n RAG_RETRIEVAL_EVENT,\n type InstrumentRetrieverOptions,\n type RagRetrievalEvent,\n} from './retrieval-telemetry.js';\nexport {\n HttpModelError,\n openAiEmbeddings,\n type OpenAiEmbeddingsOptions,\n} from './openai-embeddings.js';\nexport { HttpReranker, type HttpRerankerOptions } from './http-reranker.js';\n","export interface ChunkOptions {\n /** Target max characters per chunk. Default 800. */\n chunkSize?: number;\n /** Characters of overlap carried from the end of one chunk into the next. Default 100. */\n overlap?: number;\n /**\n * Treat the text as records joined by this string, and never split one across chunks.\n *\n * Default chunking is structure-blind: it breaks at the *latest* paragraph/sentence/word boundary\n * in the window, which for record-shaped text means a cut lands mid-record routinely. That is fine\n * for prose, where the two halves are still sentences. It is not fine for a table flattened to one\n * record per row, because the half that keeps the row identifier and the half that keeps the value\n * end up in different chunks, and neither can answer a question about that row on its own.\n *\n * With a separator set, whole records are packed greedily up to `chunkSize` and a record is never\n * cut — except one longer than `chunkSize`, which has nowhere to go and is character-split as\n * usual, since no chunker can avoid that.\n *\n * `overlap` is IGNORED in this mode, deliberately. Overlap exists to rescue a sentence that a\n * boundary cut in half; a boundary that never falls inside a record has nothing to rescue, and\n * carrying the tail of the previous chunk in would duplicate whole records into neighbouring chunks\n * — paying for the same rows twice at embedding time and letting one row match from two places.\n *\n * Measured on a 200-row spreadsheet flattened to one field-labelled record per row (15 columns,\n * ~800-char budget): blind chunking left 27% of records split, and on questions targeting a field\n * on the far side of such a cut, BM25 answered 54/66 (MRR 0.818) against 66/66 (MRR 1.000) with\n * records kept whole. The dense leg scored 7/66 either way — 200 near-identical rows produce\n * near-identical vectors, so that leg cannot do row lookup at all and chunking does not change it.\n */\n separator?: string;\n}\n\n/**\n * Splits text into overlapping chunks for embedding. Greedy up to `chunkSize`, but prefers to break\n * on a paragraph → sentence → word boundary in the back half of the window (so a chunk rarely cuts\n * mid-sentence), then carries `overlap` characters into the next chunk to preserve context across\n * the seam. Pure and deterministic.\n */\nexport function chunkText(text: string, options: ChunkOptions = {}): string[] {\n const chunkSize = options.chunkSize ?? 800;\n const overlap = options.overlap ?? 100;\n const normalized = text.trim();\n if (normalized.length === 0) {\n return [];\n }\n if (options.separator !== undefined && options.separator.length > 0) {\n return chunkByRecord(normalized, chunkSize, options.separator);\n }\n if (normalized.length <= chunkSize) {\n return [normalized];\n }\n\n const chunks: string[] = [];\n let start = 0;\n while (start < normalized.length) {\n const end = Math.min(start + chunkSize, normalized.length);\n let breakAt = end;\n if (end < normalized.length) {\n const window = normalized.slice(start, end);\n const boundary = Math.max(\n window.lastIndexOf('\\n\\n'),\n window.lastIndexOf('. '),\n window.lastIndexOf(' '),\n );\n // Only honour a boundary in the back half, else a tiny chunk cascades.\n if (boundary > chunkSize / 2) {\n breakAt = start + boundary + 1;\n }\n }\n const chunk = normalized.slice(start, breakAt).trim();\n if (chunk.length > 0) {\n chunks.push(chunk);\n }\n if (breakAt >= normalized.length) {\n break;\n }\n // Step forward with overlap, but always make progress (guards against a stuck boundary).\n start = Math.max(breakAt - overlap, start + 1);\n }\n return chunks;\n}\n\n/**\n * Pack whole records up to `chunkSize`, never splitting one. See {@link ChunkOptions.separator}.\n *\n * A record longer than the budget is handed to the blind chunker, which is the honest fallback: it is\n * the same splitting that would have happened anyway, and it is confined to the one record that could\n * not fit rather than applied to the whole document.\n */\nfunction chunkByRecord(text: string, chunkSize: number, separator: string): string[] {\n const chunks: string[] = [];\n let current = '';\n const flush = (): void => {\n if (current.length > 0) {\n chunks.push(current);\n current = '';\n }\n };\n for (const record of text.split(separator)) {\n const trimmed = record.trim();\n if (trimmed.length === 0) {\n continue;\n }\n if (trimmed.length > chunkSize) {\n flush();\n // `separator: undefined` on purpose — this is the blind path, for the one record that cannot\n // fit whole. Overlap stays off so the fallback does not reintroduce duplication the mode exists\n // to avoid.\n chunks.push(...chunkText(trimmed, { chunkSize, overlap: 0 }));\n continue;\n }\n const candidate = current.length > 0 ? `${current}${separator}${trimmed}` : trimmed;\n if (candidate.length > chunkSize) {\n flush();\n current = trimmed;\n } else {\n current = candidate;\n }\n }\n flush();\n return chunks;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport type { MetadataPatch } from './metadata-patch.js';\n\n/** A stored, embedded chunk. `embedding` length must match the store's configured dimensions. */\nexport interface VectorRecord {\n id: string;\n text: string;\n embedding: number[];\n /** Citation-facing origin (document title, URL, row id). */\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface VectorSearchOptions {\n topK: number;\n /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */\n filter?: Record<string, unknown>;\n}\n\n/**\n * The write + search side of RAG storage. `@dudousxd/nestjs-agent-rag` ships `MemoryVectorStore`\n * (in-JS cosine, tests + small scale) and `PgVectorStore` (pgvector); any impl works. Pair one with\n * an `EmbeddingProvider` via {@link import('./embedding-retriever.js').EmbeddingRetriever} to get a\n * core `Retriever`.\n *\n * Every method here is **required**, the enumeration and bulk-deletion half included. The rule for\n * where a new method goes, so the next person adding one has it rather than inferring it from\n * precedent:\n *\n * - **Required, on this interface**, when every backend *can* implement it from what it already\n * stores. {@link VectorStore.updateMetadata}, {@link VectorStore.listDocumentIds},\n * {@link VectorStore.removeMany}, {@link VectorStore.removeWhere} and\n * {@link VectorStore.countChunks} are CRUD over the records the store already holds — the same\n * footing as {@link VectorStore.remove} and\n * {@link VectorStore.listDocuments}. Making them optional would buy nothing but a permanent\n * `if (supported)` at every call site plus an unreachable \"and if it isn't?\" branch.\n * - **Optional, in a capability interface**, when some backend genuinely *cannot* without\n * infrastructure the consumer has to adopt. The only such capability today is\n * {@link LexicalVectorStore.searchText}: RediSearch has BM25 natively, Postgres would need a\n * `tsvector` column plus a GIN index plus a migration the consumer must run, and\n * `MemoryVectorStore` would have to implement BM25 itself.\n *\n * The test is whether a backend can do it at all — not whether adding it would inconvenience an\n * implementer who has already shipped.\n *\n * Implementations MUST apply every filter with exactly the same semantics as\n * {@link VectorStore.search} — including the empty-array deny primitive — because the filter is\n * routinely an access-control boundary, not a hint. On {@link VectorStore.removeWhere} that is not a\n * correctness nicety but the single most destructive thing an implementation could get wrong.\n */\nexport interface VectorStore {\n upsert(records: VectorRecord[]): Promise<void>;\n search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;\n /**\n * Delete every chunk belonging to a source document — all records whose id is `${documentId}`\n * or `${documentId}#<n>` (the id scheme {@link import('./ingest.js').chunkDocuments} produces).\n * Use it to drop a document from the index, and before re-ingesting one: `upsert` overwrites\n * matching ids but can't remove chunks a shorter new version no longer produces, so a re-ingest\n * without a preceding `remove` leaves the old tail orphaned.\n */\n remove(documentId: string): Promise<void>;\n /**\n * Rewrite a document's metadata **without touching its text or its embeddings** — the update path\n * for a dimension that is genuinely mutable (which bases a document is relevant to, who may see it,\n * what it was reclassified as) rather than derived from its content.\n *\n * Without this, a consumer whose documents get re-classified has exactly two options, and both are\n * bad: re-embed the whole document — paying the model bill, and burning the ingestion budget, to\n * change a string — or refuse to stamp the dimension onto chunks at all and resolve it at query\n * time instead, which turns a filter the index could have applied into a join the caller has to.\n *\n * `patch` is applied to **every chunk** of the document (metadata is a document-level property that\n * happens to be stored per chunk), with the merge semantics documented on {@link MetadataPatch}:\n * partial merge, `null` to remove a key, `undefined` ignored, values — including arrays — replaced\n * wholesale.\n *\n * Returns **the number of chunks written**. An unknown `documentId` updates nothing and returns\n * `0`; it does not throw. That is deliberate and matches {@link VectorStore.remove}, which is\n * likewise silent on a document that isn't there: this is a reconciliation-shaped API, called by a\n * loop diffing a source of truth against the index, and such a loop races with ingestion and\n * deletion by construction. Turning \"the document was removed between the diff and the write\" into\n * an exception would make the ordinary case an error, and the caller would have to catch and\n * swallow it to get back the behaviour it wanted. The count gives the same information without\n * that: `0` means nothing was there. (An empty patch also writes nothing and returns `0` — the\n * return counts chunks *written*, not chunks *matched*.)\n *\n * Implementations MUST keep every representation of the metadata in step. A store that keeps both\n * a filterable projection and a returned blob (RediSearch's `meta_*` TAGs beside its\n * `metadata_json`) and updates only one makes the store lie to itself: a chunk that *filters* as\n * one value but *reports* another, where which half you notice depends on whether you searched or\n * read.\n */\n updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;\n /**\n * List the distinct source documents currently indexed (chunk ids collapsed back to their document\n * by stripping the trailing `#<n>`), each with a representative chunk's `metadata`, optionally\n * narrowed by a metadata `filter`. The enumeration seam for reconciliation: diff this against your\n * source of truth to find documents to ingest (missing), re-ingest (a stored fingerprint like\n * `size` changed), or {@link VectorStore.remove remove} (orphaned).\n */\n listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;\n\n /**\n * Read one document's chunks back — id, text, metadata — ordered by their position in the\n * document, optionally paged with `limit`/`offset`.\n *\n * The gap this closes is that **the text a store holds is otherwise write-only**. Every other way\n * back to it is a ranked query: {@link VectorStore.search} needs an embedding,\n * {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,\n * in relevance order, capped at `topK`. Neither can answer \"show me what was indexed for this\n * document, in order\" — the question you ask when retrieval returns the wrong passage and you need\n * to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to\n * see stored text is to reach around the adapter into the backing engine, which is exactly the\n * abstraction break a consumer adopts a store to avoid.\n *\n * Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and\n * {@link VectorStore.updateMetadata}: ordering is the whole point, and \"position within the\n * document\" is only meaningful once a document has been picked. Enumerate documents with\n * {@link VectorStore.listDocuments} first when you need to sweep.\n *\n * An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.\n *\n * NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce\n * one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST\n * resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately\n * shaped for an operator/debug surface, not for serving end users.\n */\n listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;\n\n /**\n * The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —\n * {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a\n * reconciliation diff against your source of truth, a count, a delete list): implementations are\n * expected to skip fetching and parsing per-chunk metadata entirely, which is most of the cost of\n * `listDocuments` on a large corpus.\n *\n * An empty-array filter value denies, yielding `[]`.\n */\n listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;\n\n /**\n * Delete every chunk of every one of `documentIds`, with the same per-document semantics as\n * {@link VectorStore.remove} — but as one bulk operation rather than N independent ones. An empty\n * array is a no-op.\n *\n * The gap this closes is asymptotic, not cosmetic: on the Redis adapter each `remove` is a `SCAN`\n * that walks the whole keyspace, which in a real deployment also holds queues, workflow state and\n * the app cache. Two thousand documents is two thousand full passes; this is one filtered query.\n */\n removeMany(documentIds: string[]): Promise<void>;\n\n /**\n * Delete every chunk matching a metadata `filter`, and resolve the number of **chunks** removed\n * (not documents). The collection-maintenance primitive: dropping a collection, evicting a tenant,\n * purging a retired audience.\n *\n * Two guard rails, because this is the one method here that destroys data:\n *\n * - **The empty-array deny is honoured, and it means \"delete nothing\".** `removeWhere({ audience: [] })`\n * removes zero chunks. Treating a deny as \"no filter\" would delete the entire corpus, which is\n * exactly inverted from what the caller asked for.\n * - **The filter must be non-empty.** `removeWhere({})` throws {@link UnsafeRemovalError} rather\n * than wiping the store, because an empty object is overwhelmingly more likely to be a filter that\n * got built wrong (a dropped key, an `undefined` scope) than a deliberate request to delete\n * everything. Deliberate mass deletion stays available and stays explicit:\n * `removeMany(await store.listDocumentIds())`, or the backend's own drop.\n *\n * What it removes is exactly what a {@link VectorStore.search} carrying the same filter could\n * reach — no more (it cannot escape the filter) and no less.\n */\n removeWhere(filter: Record<string, unknown>): Promise<number>;\n\n /**\n * How many **chunks** match `filter` (all of them when it is omitted) — a counted query, without\n * transferring the chunks. Sizing a reindex, reporting a collection's footprint, and confirming a\n * {@link VectorStore.removeWhere} did what you expected. An empty-array filter value denies,\n * yielding `0`.\n */\n countChunks(filter?: Record<string, unknown>): Promise<number>;\n}\n\n/**\n * An **optional** capability a {@link VectorStore} may also implement: full-text (lexical) search\n * over the chunk text it already stores, using the backing engine's own keyword index — RediSearch's\n * BM25 over the `TEXT` field, say. Its point is the multi-process deployment: when ingestion runs on\n * a worker and search runs on an API pod, the API process never sees the chunks, so the in-process\n * {@link import('./keyword-retriever.js').KeywordRetriever} has nothing to index. A store that\n * implements this needs no second index, no corpus in JS heap, and no refresh window — a chunk is\n * lexically findable the moment it is upserted.\n *\n * Wrap it as a core `Retriever` with {@link import('./lexical-retriever.js').LexicalRetriever}.\n *\n * Implementations MUST apply `options.filter` with exactly the same semantics as\n * {@link VectorStore.search} — including the empty-array deny primitive — because the filter is\n * routinely an access-control boundary, not a hint.\n */\nexport interface LexicalVectorStore extends VectorStore {\n /**\n * Rank stored chunks against a natural-language `query` by keyword relevance. `options.filter`\n * narrows exactly as in {@link VectorStore.search}. The returned `score` is the engine's own\n * relevance score (BM25), which shares no scale with `search`'s cosine similarity — fuse the two\n * with {@link import('./hybrid-retriever.js').HybridRetriever}, whose RRF is rank-based and so\n * needs no common scale.\n */\n searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;\n}\n\n/**\n * Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the\n * store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)\n * when it is not, without knowing which adapter it was handed.\n */\nexport function isLexicalVectorStore(store: VectorStore): store is LexicalVectorStore {\n return typeof (store as Partial<LexicalVectorStore>).searchText === 'function';\n}\n\n/**\n * A destructive call was refused because its scope could not be trusted. Thrown by\n * {@link VectorStore.removeWhere} — never for something that merely matched nothing (that\n * is a legitimate result of `0`), only for a filter that would have deleted *more* than the caller\n * plausibly meant.\n */\nexport class UnsafeRemovalError extends Error {\n constructor(\n /** Why the removal was refused. */\n readonly reason: 'empty-filter' | 'unindexed-field',\n message: string,\n ) {\n super(message);\n this.name = 'UnsafeRemovalError';\n }\n}\n\n/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */\nexport interface IndexedDocument {\n id: string;\n metadata?: Record<string, unknown>;\n}\n\n/** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */\nexport interface StoredChunk {\n /** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */\n id: string;\n /** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */\n index: number;\n /** The chunk text as it was indexed — the exact string the embedding was computed from. */\n text: string;\n metadata?: Record<string, unknown>;\n}\n\n/** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */\nexport interface ListChunksOptions {\n /** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */\n limit?: number;\n /** Chunks to skip, applied after ordering. */\n offset?: number;\n}\n\n/** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */\nexport function chunkIndexOf(chunkId: string): number {\n const match = /#(\\d+)$/.exec(chunkId);\n return match?.[1] === undefined ? 0 : Number(match[1]);\n}\n\n/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */\nexport function documentIdOf(chunkId: string): string {\n return chunkId.replace(/#\\d+$/, '');\n}\n","/**\n * A partial update to a document's metadata, as taken by\n * {@link import('./vector-store.js').VectorStore.updateMetadata}. Deliberately *partial*: the useful\n * shape is \"this one dimension was reclassified\", not \"here is the whole metadata object again\" —\n * a caller that had the whole object would have to reconstruct fields it does not own (the ingestion\n * fingerprint, the source, the mime type) just to change one string, and would silently drop them\n * the day the library adds another.\n *\n * The semantics are RFC 7386 (JSON Merge Patch), with one deliberate narrowing:\n *\n * - **Merge, not replace.** Keys present in the patch are written; keys absent from it keep their\n * stored value.\n * - **`null` removes the key.** That is the RFC's sentinel and it is the *only* way to remove one.\n * The consequence — a literal `null` cannot be stored as a value — is intended: metadata is a\n * filter surface, and \"the key is absent\" and \"the key is null\" would filter identically anyway.\n * - **`undefined` is ignored** — the key is left exactly as stored, as if the patch had not mentioned\n * it. `undefined` does not survive `JSON.stringify`, and a patch routinely crosses a JSON boundary\n * (an HTTP body, a durable-workflow step payload) on its way here; honouring it would make\n * `{ audience: undefined }` mean *delete* on one side of that boundary and *no-op* on the other.\n * It is also what `{ ...doc, audience: doc.audience }` produces by accident, and an accidental\n * deletion of an access-control dimension is the worst outcome this API can have. Removing a key\n * is therefore always spelled `null`.\n * - **Shallow.** A value replaces wholesale; it is not merged into the stored value. This is the\n * narrowing versus RFC 7386, which recurses into nested objects. It matters most for **arrays**,\n * where wholesale replacement is certainly what a caller means: `{ bases: ['A', 'C'] }` sets the\n * document's bases to exactly A and C — it does not append to, or union with, what was there. An\n * array-valued key is a *set-valued dimension* (the multi-valued TAG a store filters on), and the\n * only sane way to shrink such a set is to state the new one.\n */\nexport type MetadataPatch = Record<string, unknown>;\n\n/**\n * Split a patch into the assignments it makes and the keys it removes — the shape a store that\n * updates metadata declaratively (SQL, a hash write + field delete) needs, since setting and\n * unsetting are different operations there. `undefined` values appear in neither.\n */\nexport function splitMetadataPatch(patch: MetadataPatch): {\n set: Record<string, unknown>;\n remove: string[];\n} {\n const set: Record<string, unknown> = {};\n const remove: string[] = [];\n for (const [key, value] of Object.entries(patch)) {\n if (value === null) {\n remove.push(key);\n } else if (value !== undefined) {\n set[key] = value;\n }\n }\n return { set, remove };\n}\n\n/** Does this patch write anything at all? An empty (or all-`undefined`) patch is a no-op. */\nexport function isEmptyMetadataPatch(patch: MetadataPatch): boolean {\n const { set, remove } = splitMetadataPatch(patch);\n return Object.keys(set).length === 0 && remove.length === 0;\n}\n\n/**\n * Apply a {@link MetadataPatch} to a document's stored metadata, returning a NEW object — the\n * reference implementation of the semantics documented on `MetadataPatch`, so every adapter that\n * merges in JS agrees with every other one. Never mutates its input: `MemoryVectorStore` hands out\n * the very object a caller upserted, and quietly rewriting it under them would be a surprise no\n * other store reproduces.\n */\nexport function applyMetadataPatch(\n metadata: Record<string, unknown> | undefined,\n patch: MetadataPatch,\n): Record<string, unknown> {\n const { set, remove } = splitMetadataPatch(patch);\n const next: Record<string, unknown> = { ...metadata, ...set };\n for (const key of remove) {\n delete next[key];\n }\n return next;\n}\n","/** Metadata filter, shared by the in-memory {@link import('./memory-vector-store.js').MemoryVectorStore}\n * and {@link import('./keyword-retriever.js').KeywordRetriever}. Every key must match:\n *\n * - a **scalar** filter value keeps exact-match — the record's value must equal it;\n * - an **array** filter value means **match-any** (OR / set membership) — the record matches when its\n * value for that key is one of them, or (for a multi-valued record) shares at least one element.\n * An empty array matches nothing (no value is a member of the empty set) — the deny primitive.\n *\n * Array-valued filters are how a caller expresses capability-style access control over a shared store\n * (e.g. `{ audience: ['public', 'role:ADMIN', 'base:…'] }`) without the store knowing what a token means.\n */\nexport function matchesFilter(\n metadata: Record<string, unknown> | undefined,\n filter: Record<string, unknown>,\n): boolean {\n const entries = Object.entries(filter);\n if (metadata === undefined) {\n return entries.length === 0;\n }\n return entries.every(([key, expected]) => {\n if (Array.isArray(expected)) {\n const actual = metadata[key];\n return Array.isArray(actual)\n ? actual.some((value) => expected.includes(value))\n : expected.includes(actual);\n }\n return metadata[key] === expected;\n });\n}\n\n/**\n * Does this filter provably match **nothing**? True when any key carries an empty array, because\n * nothing is a member of the empty set and every key must match (AND). This is the package's **deny\n * primitive** — the shape a capability-style ACL collapses to for an actor holding no tokens — so\n * every store has to honour it identically, and a store that cannot express \"match nothing\" in its\n * query language (RediSearch has no valid empty-tag syntax) must short-circuit on it instead.\n *\n * It is exported from this module rather than reimplemented per adapter precisely because getting it\n * wrong is *silent*: a read path that ignores it over-returns, and a **write** path that ignores it\n * (see {@link import('./vector-store.js').VectorStore.removeWhere}) deletes the corpus a\n * deny was supposed to protect.\n */\nexport function filterMatchesNothing(filter?: Record<string, unknown>): boolean {\n if (filter === undefined) {\n return false;\n }\n return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing, matchesFilter } from './filter.js';\nimport { type MetadataPatch, applyMetadataPatch, isEmptyMetadataPatch } from './metadata-patch.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport {\n type IndexedDocument,\n type ListChunksOptions,\n type StoredChunk,\n UnsafeRemovalError,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n chunkIndexOf,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter\n * for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).\n *\n * It implements the enumeration and bulk-deletion half of {@link VectorStore} too. There is no\n * performance argument for it here — every method below is a loop over the same Map — but a store that tests are\n * written against has to behave identically to the one production runs, and the semantics that matter\n * (`removeWhere` refusing an empty filter, an empty-array filter deleting nothing) are behavioural,\n * not incidental to Redis.\n */\nexport class MemoryVectorStore implements VectorStore {\n private readonly records = new Map<string, VectorRecord>();\n\n /**\n * Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this\n * instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them\n * apart on a dashboard.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'memory' };\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n for (const id of this.records.keys()) {\n if (documentIdOf(id) === documentId) {\n this.records.delete(id);\n }\n }\n }\n\n /**\n * Merge `patch` into every chunk of `documentId`, leaving `text` and `embedding` untouched. The\n * record is *replaced* rather than mutated: `upsert` stores the caller's own object, and patching\n * it in place would edit an object the caller still holds — a surprise no out-of-process store\n * could reproduce, and therefore one this reference adapter must not teach.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n let updated = 0;\n for (const [id, record] of this.records) {\n if (documentIdOf(id) !== documentId) {\n continue;\n }\n this.records.set(id, { ...record, metadata: applyMetadataPatch(record.metadata, patch) });\n updated += 1;\n }\n return updated;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const documents = new Map<string, IndexedDocument>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n const id = documentIdOf(record.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return [...documents.values()];\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * Insertion order is not document order — `upsert` stores whatever the caller passed, and a\n * re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index\n * parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the\n * Map. A reference adapter that returned \"usually ordered\" would let a consumer ship a bug that\n * only shows up on the store tests are NOT written against.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n const chunks: StoredChunk[] = [];\n for (const record of this.records.values()) {\n if (documentIdOf(record.id) !== documentId) {\n continue;\n }\n chunks.push({\n id: record.id,\n index: chunkIndexOf(record.id),\n text: record.text,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n chunks.sort((a, b) => a.index - b.index);\n const offset = options?.offset ?? 0;\n return options?.limit === undefined\n ? chunks.slice(offset)\n : chunks.slice(offset, offset + options.limit);\n }\n\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const documents = new Set<string>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n documents.add(documentIdOf(record.id));\n }\n return [...documents];\n }\n\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n const wanted = new Set(documentIds);\n for (const id of this.records.keys()) {\n if (wanted.has(documentIdOf(id))) {\n this.records.delete(id);\n }\n }\n }\n\n /**\n * See {@link VectorStore.removeWhere}. The empty-array short-circuit below is redundant\n * against `matchesFilter` (which already matches nothing for an empty array) and is kept anyway:\n * this is the method where \"the filter accidentally means everything\" destroys data, so the deny is\n * stated where a reader can see it rather than inferred from another module's truth table.\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n if (Object.keys(filter).length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the store. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n let removed = 0;\n for (const [id, record] of this.records) {\n if (matchesFilter(record.metadata, filter)) {\n this.records.delete(id);\n removed += 1;\n }\n }\n return removed;\n }\n\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n let count = 0;\n for (const record of this.records.values()) {\n if (filter === undefined || matchesFilter(record.metadata, filter)) {\n count += 1;\n }\n }\n return count;\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const scored: Passage[] = [];\n for (const record of this.records.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n scored.push({\n id: record.id,\n text: record.text,\n score: cosineSimilarity(embedding, record.embedding),\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK);\n }\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n const length = Math.min(a.length, b.length);\n for (let index = 0; index < length; index += 1) {\n const valueA = a[index] ?? 0;\n const valueB = b[index] ?? 0;\n dot += valueA * valueB;\n normA += valueA * valueA;\n normB += valueB * valueB;\n }\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dot / denominator;\n}\n","/**\n * Turning a natural-language question into full-text search terms, for the lexical leg of\n * `PgLexicalVectorStore`.\n *\n * With the `simple` text-search configuration (no stemming, no stop words; any language) a question\n * searched as is either matches nothing (every word required) or, as \"any word\", ranks chunks full\n * of \"the / of / which / de / que\" first. So the query side drops the stop words of the question's\n * language(s) and keeps the meaningful terms, which the store then ranks by how many of them a chunk\n * holds, rare terms weighing more (an IDF over the matching rows, BM25-style).\n */\n\nconst words = (s: string) => new Set(s.split(/\\s+/).filter(Boolean));\n\n/**\n * Postgres' Snowball stop word lists (`tsearch_data/{english,portuguese,spanish}.stop`), minus\n * one-letter words (never terms here) and words that carry meaning in a sibling language\n * (\"estado\", \"sentido\").\n */\nexport const DEFAULT_STOP_WORDS: Readonly<Record<string, ReadonlySet<string>>> = {\n english: words(`\n me my myself we our ours ourselves you your yours yourself yourselves he him his himself she\n her hers herself it its itself they them their theirs themselves what which who whom this that\n these those am is are was were be been being have has had having do does did doing an the and\n but if or because as until while of at by for with about against between into through during\n before after above below to from up down in out on off over under again further then once here\n there when where why how all any both each few more most other some such no nor not only own\n same so than too very can will just don should now`),\n portuguese: words(`\n de que do da em um para com não uma os no se na por mais as dos como mas ao ele das seu sua ou\n quando muito nos já eu também só pelo pela até isso ela entre depois sem mesmo aos seus quem\n nas me esse eles você essa num nem suas meu às minha numa pelos elas qual nós lhe deles essas\n esses pelas este dele tu te vocês vos lhes meus minhas teu tua teus tuas nosso nossa nossos\n nossas dela delas esta estes estas aquele aquela aqueles aquelas isto aquilo estou está estamos\n estão estive esteve estivemos estiveram estava estávamos estavam estivera estivéramos esteja\n estejamos estejam estivesse estivéssemos estivessem estiver estivermos estiverem hei há havemos\n hão houve houvemos houveram houvera houvéramos haja hajamos hajam houvesse houvéssemos\n houvessem houver houvermos houverem houverei houverá houveremos houverão houveria houveríamos\n houveriam sou somos são era éramos eram fui foi fomos foram fora fôramos seja sejamos sejam\n fosse fôssemos fossem for formos forem serei será seremos serão seria seríamos seriam tenho tem\n temos tém tinha tínhamos tinham tive teve tivemos tiveram tivera tivéramos tenha tenhamos\n tenham tivesse tivéssemos tivessem tiver tivermos tiverem terei terá teremos terão teria\n teríamos teriam`),\n spanish: words(`\n de la que el en los del se las por un para con no una su al lo como más pero sus le ya este sí\n porque esta entre cuando muy sin sobre también me hasta hay donde quien desde todo nos durante\n todos uno les ni contra otros ese eso ante ellos esto mí antes algunos qué unos yo otro otras\n otra él tanto esa estos mucho quienes nada muchos cual poco ella estar estas algunas algo\n nosotros mi mis tú te ti tu tus ellas nosotras vosotros vosotras os mío mía míos mías tuyo\n tuya tuyos tuyas suyo suya suyos suyas nuestro nuestra nuestros nuestras vuestro vuestra\n vuestros vuestras esos esas estoy estás está estamos estáis están esté estés estemos estéis\n estén estaré estarás estará estaremos estaréis estarán estaría estarías estaríamos estaríais\n estarían estaba estabas estábamos estabais estaban estuve estuviste estuvo estuvimos\n estuvisteis estuvieron estuviera estuvieras estuviéramos estuvierais estuvieran estuviese\n estuvieses estuviésemos estuvieseis estuviesen estando estad he has ha hemos habéis han haya\n hayas hayamos hayáis hayan habré habrás habrá habremos habréis habrán habría habrías habríamos\n habríais habrían había habías habíamos habíais habían hube hubiste hubo hubimos hubisteis\n hubieron hubiera hubieras hubiéramos hubierais hubieran hubiese hubieses hubiésemos hubieseis\n hubiesen habiendo habido habida habidos habidas soy eres es somos sois son sea seas seamos\n seáis sean seré serás será seremos seréis serán sería serías seríamos seríais serían era eras\n éramos erais eran fui fuiste fue fuimos fuisteis fueron fuera fueras fuéramos fuerais fueran\n fuese fueses fuésemos fueseis fuesen tengo tienes tiene tenemos tenéis tienen tenga tengas\n tengamos tengáis tengan tendré tendrás tendrá tendremos tendréis tendrán tendría tendrías\n tendríamos tendríais tendrían tenía tenías teníamos teníais tenían tuve tuviste tuvo tuvimos\n tuvisteis tuvieron tuviera tuvieras tuviéramos tuvierais tuvieran tuviese tuvieses tuviésemos\n tuvieseis tuviesen teniendo tenido tenida tenidos tenidas tened`),\n};\n\nconst TOKEN = /[\\p{L}\\p{N}]{2,}/gu;\n\n/**\n * The meaningful terms of a question, in order, deduplicated, at most `max`: the stop words of the\n * language(s) the question is written in are dropped (the language(s) with the most stop words in\n * it, so \"sea\"/\"son\" stay terms in an English question although they are Spanish stop words). A\n * question made only of stop words keeps them all (there is nothing else to search for).\n */\nexport function keywordTerms(\n query: string,\n max = 24,\n stopWords: Readonly<Record<string, ReadonlySet<string>>> = DEFAULT_STOP_WORDS,\n): string[] {\n const tokens = [...new Set(query.toLowerCase().normalize('NFC').match(TOKEN) ?? [])];\n const counts = Object.entries(stopWords).map(\n ([, stop]) => [stop, tokens.filter((t) => stop.has(t)).length] as const,\n );\n const best = Math.max(0, ...counts.map(([, n]) => n));\n if (best === 0) return tokens.slice(0, max);\n const stop = counts.filter(([, n]) => n === best).map(([s]) => s);\n const terms = tokens.filter((t) => !stop.some((s) => s.has(t)));\n return (terms.length > 0 ? terms : tokens).slice(0, max);\n}\n\n/** Whether the user wrote search syntax (a quoted phrase, `-exclusion`) worth honoring as such. */\nexport function hasSearchSyntax(query: string): boolean {\n return /\"[^\"]+\"/.test(query) || /(^|\\s)-[\\p{L}\\p{N}]/u.test(query);\n}\n\n/** `'a' | 'b' | …` for `to_tsquery` (terms are letter/digit runs, so quoting is enough). */\nexport function anyTermTsquery(terms: string[]): string {\n return terms.map((t) => `'${t.replace(/'/g, '')}'`).join(' | ');\n}\n","/**\n * The minimal Postgres surface {@link import('./pg-vector-store.js').PgVectorStore} needs — adapt your\n * own `pg` / `postgres.js` client to it, so this package pulls in NO driver (bring your own, like the\n * store adapters take an ORM handle and the Redis transport takes a client). `query` runs\n * parameterized SQL (`$1,$2,…`) and resolves the result rows.\n *\n * `transaction` is optional. When present, the store uses it for the few reads that must share one\n * connection with a `SET LOCAL` (pgvector's iterative index scan); without it those settings are\n * simply not applied. You rarely write either by hand: pass a `pg` `Pool`, a Drizzle database or a\n * `postgres.js` `sql` and {@link toPgClient} builds both.\n */\nexport interface PgClient {\n query<Row = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<Row[]>;\n /** Run `work` inside one transaction on one connection; commit on success, roll back on throw. */\n transaction?<T>(work: (tx: PgClient) => Promise<T>): Promise<T>;\n}\n\n/**\n * A node-postgres `Pool`, `PoolClient` or `Client` — anything whose `query(text, values)` resolves a\n * result with `rows`. Typed structurally so this package needs no `pg` import.\n */\nexport interface NodePgQueryable {\n query(text: string, values?: any[]): Promise<unknown>;\n}\n\n/** A `postgres.js` `sql` instance (or a transaction handle from `sql.begin`). */\nexport interface PostgresJsSql {\n unsafe(query: string, params?: any[]): PromiseLike<unknown>;\n begin?(...args: any[]): PromiseLike<unknown>;\n}\n\n/**\n * A Drizzle database — `drizzle(pool)` for node-postgres or `drizzle(sql)` for postgres.js. Drizzle\n * exposes the driver it wraps as `$client`, which is what the store talks to; this saves the\n * `(db as unknown as { $client: Pool }).$client` cast at every call site.\n */\nexport interface DrizzleLike {\n $client: NodePgQueryable | PostgresJsSql;\n}\n\n/** Everything {@link toPgClient} (and so the `PgVectorStore` constructor) accepts. */\nexport type PgClientSource = PgClient | NodePgQueryable | PostgresJsSql | DrizzleLike;\n\nconst ADAPTED = Symbol.for('@dudousxd/nestjs-agent-rag/pg-client');\n\n/**\n * Normalize a Postgres handle into a {@link PgClient}:\n *\n * - a Drizzle database → its `$client`, then one of the below;\n * - a `postgres.js` `sql` → `sql.unsafe(text, params)`, transactions via `sql.begin`;\n * - a node-postgres `Pool` → `pool.query(...).rows`, transactions on a checked-out client\n * (`BEGIN`/`COMMIT`/`ROLLBACK`, then `release()`); a single `Client`/`PoolClient` runs its\n * transaction in place;\n * - a hand-written {@link PgClient} (`query` resolving the rows array) passes through unchanged.\n *\n * Detection is structural and happens per call result for `query` (an array is taken as the rows, an\n * object's `rows` otherwise), so a hand-written adapter keeps working exactly as before.\n */\nexport function toPgClient(source: PgClientSource): PgClient {\n if ((source as { [ADAPTED]?: true })[ADAPTED]) {\n return source as PgClient;\n }\n if (isDrizzle(source)) {\n return toPgClient(source.$client);\n }\n if (typeof (source as PostgresJsSql).unsafe === 'function') {\n return fromPostgresJs(source as PostgresJsSql);\n }\n return fromQueryable(source as NodePgQueryable & Partial<PgClient>);\n}\n\nfunction isDrizzle(source: PgClientSource): source is DrizzleLike {\n const client = (source as Partial<DrizzleLike>).$client;\n return client !== null && client !== undefined && typeof client === 'object';\n}\n\nfunction mark(client: PgClient): PgClient {\n Object.defineProperty(client, ADAPTED, { value: true });\n return client;\n}\n\nfunction rowsOf<Row>(result: unknown): Row[] {\n if (Array.isArray(result)) {\n return result as Row[];\n }\n const rows = (result as { rows?: unknown } | null | undefined)?.rows;\n return Array.isArray(rows) ? (rows as Row[]) : [];\n}\n\nfunction fromPostgresJs(sql: PostgresJsSql): PgClient {\n const client: PgClient = {\n query: async <Row>(text: string, params: unknown[] = []) =>\n // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.\n [...rowsOf<Row>(await sql.unsafe(text, params))],\n };\n if (typeof sql.begin === 'function') {\n const begin = sql.begin.bind(sql);\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) =>\n Promise.resolve(begin((tx: PostgresJsSql) => work(fromPostgresJs(tx)))) as Promise<T>;\n }\n return mark(client);\n}\n\ninterface NodePgPoolLike extends NodePgQueryable {\n connect(): Promise<NodePgQueryable & { release(error?: unknown): void }>;\n totalCount: number;\n}\n\nfunction fromQueryable(source: NodePgQueryable & Partial<PgClient>): PgClient {\n const client: PgClient = {\n query: async <Row>(text: string, params?: unknown[]) =>\n rowsOf<Row>(await source.query(text, params)),\n };\n if (typeof source.transaction === 'function') {\n // A hand-written PgClient that already knows how to run a transaction.\n const transaction = source.transaction.bind(source);\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) =>\n transaction((tx: PgClient) => work(toPgClient(tx)));\n } else if (isNodePgPool(source)) {\n client.transaction = async <T>(work: (tx: PgClient) => Promise<T>) => {\n const connection = await source.connect();\n let broken = false;\n try {\n return await runInTransaction(connection, work, () => {\n broken = true;\n });\n } finally {\n // A connection whose ROLLBACK failed is not safe to hand back to the pool: a truthy\n // argument makes node-postgres destroy it instead.\n connection.release(broken ? new Error('ROLLBACK failed') : undefined);\n }\n };\n } else if (isNodePgClient(source)) {\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) => runInTransaction(source, work);\n }\n return mark(client);\n}\n\nasync function runInTransaction<T>(\n connection: NodePgQueryable,\n work: (tx: PgClient) => Promise<T>,\n onRollbackFailed?: () => void,\n): Promise<T> {\n const tx = mark({\n query: async <Row>(text: string, params?: unknown[]) =>\n rowsOf<Row>(await connection.query(text, params)),\n });\n await connection.query('BEGIN');\n try {\n const result = await work(tx);\n await connection.query('COMMIT');\n return result;\n } catch (error) {\n // The original error is what the caller needs; a failed ROLLBACK only poisons the connection.\n await connection.query('ROLLBACK').catch(() => onRollbackFailed?.());\n throw error;\n }\n}\n\nfunction isNodePgPool(source: object): source is NodePgPoolLike {\n return (\n typeof (source as Partial<NodePgPoolLike>).connect === 'function' &&\n typeof (source as Partial<NodePgPoolLike>).totalCount === 'number'\n );\n}\n\n/** A node-postgres `Client`/`PoolClient`: one connection, so a transaction runs on it directly. */\nfunction isNodePgClient(source: object): boolean {\n return (\n typeof (source as { release?: unknown }).release === 'function' ||\n typeof (source as { processID?: unknown }).processID === 'number' ||\n (source as { connectionParameters?: unknown }).connectionParameters !== undefined\n );\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing } from './filter.js';\nimport {\n DEFAULT_STOP_WORDS,\n anyTermTsquery,\n hasSearchSyntax,\n keywordTerms,\n} from './lexical-query.js';\nimport { type MetadataPatch, isEmptyMetadataPatch, splitMetadataPatch } from './metadata-patch.js';\nimport { type PgClient, type PgClientSource, toPgClient } from './pg-client.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport type {\n IndexedDocument,\n LexicalVectorStore,\n ListChunksOptions,\n StoredChunk,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nimport { UnsafeRemovalError } from './vector-store.js';\n\nexport type { PgClient } from './pg-client.js';\n\n/** Where the lexical leg of a {@link PgLexicalVectorStore} reads its `tsvector` from. */\nexport interface PgFullTextOptions {\n /**\n * Text-search configuration (`simple`, `english`, `portuguese`, …). Default `simple` — no stemming\n * or stop words, so it works for any language and matches exact terms, which is what the lexical\n * leg of a hybrid search is for. Must be the same configuration the index was built with, or\n * Postgres cannot use the index and scans the table (still correct, just slow).\n */\n config?: string;\n /**\n * A `tsvector` column you maintain yourself (typically `GENERATED ALWAYS AS (…) STORED` in a\n * migration, e.g. weighting a title above the body), with its own GIN index. When omitted the\n * store searches the expression `to_tsvector(config, text)`, and `ensureSchema` creates a GIN\n * expression index over exactly that.\n */\n column?: string;\n /**\n * Search natural-language questions by their meaningful terms (default `true`): the question's\n * stop words are dropped (see {@link PgFullTextOptions.stopWords}) and rows holding any remaining\n * term (at most 24) are ranked by the sum of the matched terms' IDF among those rows (BM25\n * without term frequencies), `ts_rank_cd` breaking ties; a question with explicit search syntax\n * (a quoted phrase, `-word`) is first tried as written. `false`: only `websearch_to_tsquery` on\n * the query as written (every word must appear).\n */\n anyTermFallback?: boolean;\n /**\n * Stop word lists dropped from questions, by language; only the list(s) with the most words in\n * the question apply, so a word that is a stop word in another language stays a term. Default\n * {@link DEFAULT_STOP_WORDS} (English, Portuguese, Spanish: Postgres' Snowball lists). `false`\n * keeps every word. With a language `config` (`english`) Postgres drops that language's stop\n * words as well.\n */\n stopWords?: Readonly<Record<string, ReadonlySet<string>>> | false;\n}\n\nexport interface PgVectorStoreOptions {\n /** Table name. Default `agent_rag_chunks`. */\n table?: string;\n /**\n * Embedding width — must match your model (e.g. 1536 for text-embedding-3-small). Default 1536.\n *\n * Pass a **list** to let several models' vectors share one table (mixed dimensions, e.g. while\n * migrating from a 768-wide local model to a 1536-wide hosted one): the column becomes an untyped\n * `vector`, `ensureSchema` creates one *partial* HNSW index per listed width\n * (`((embedding::vector(n))) … WHERE vector_dims(embedding) = n`), and `search` only compares the\n * query against vectors of its own width, through that width's index. A query whose width is not\n * listed is still answered — exactly, by a sequential scan. Filter on the model as well (put it in\n * the chunk metadata) when two models share a width.\n */\n dimensions?: number | readonly number[];\n /**\n * Allow chunks with no embedding (`embedding: []` on upsert is stored as `NULL`): index text first\n * and embed later, or keep full-text-only chunks when there is no embedding model at all. The\n * vector leg skips them; the lexical leg of {@link PgLexicalVectorStore} still finds them. Default\n * `false` — the column stays `NOT NULL`. Only affects the DDL `ensureSchema` emits for a new table.\n */\n nullableEmbeddings?: boolean;\n /**\n * pgvector ≥ 0.8 iterative index scans for `search`: keep walking the HNSW graph until enough rows\n * pass the metadata filter, instead of returning fewer than `topK` when a selective filter (one\n * tenant of many) rejects most of the candidates the index produced. Applied with `SET LOCAL`, so\n * it needs a client with a `transaction` (a `pg` Pool, Drizzle or postgres.js all have one); it is\n * silently skipped on an older pgvector or a client without transactions. Default off.\n */\n iterativeScan?: 'strict_order' | 'relaxed_order';\n /** `hnsw.ef_search` for `search` (candidate list size; pgvector's default is 40). Needs a `transaction`. */\n efSearch?: number;\n /**\n * Rows per multi-row `INSERT … ON CONFLICT` statement in `upsert`. Default 100. Duplicate ids in\n * one `upsert` call collapse to the last occurrence (as the old row-by-row loop left them).\n */\n upsertBatchSize?: number;\n}\n\n/** The one byte Postgres `text`/`jsonb` columns refuse outright. */\nconst NUL_BYTE = String.fromCharCode(0);\n\n/**\n * Strip the NUL byte (U+0000) out of a value before it reaches Postgres. `text` and `jsonb` columns\n * reject it outright — `invalid byte sequence for encoding \"UTF8\": 0x00` — and text extracted from\n * PDFs occasionally carries one even though other stores (Qdrant) accept it happily, so a chunk that\n * ingested fine elsewhere can fail here. Strings have the byte removed; arrays and **plain** objects\n * are walked recursively (object keys included, since a NUL can land in a metadata key too).\n *\n * \"Plain\" is deliberately narrow — `Object.getPrototypeOf(value)` must be `Object.prototype` or\n * `null` — so a `Date`, a `Buffer`, a class instance or any other non-plain object passes through\n * untouched rather than collapsing to `{}` via a blind `Object.entries`. Rebuilt objects go through\n * `Object.fromEntries` rather than assignment into a fresh `{}`, so a metadata key literally named\n * `__proto__` becomes an ordinary own property instead of silently repointing the result's prototype.\n */\nfunction stripNulBytes<T>(value: T): T {\n if (typeof value === 'string') {\n return (value.includes(NUL_BYTE) ? value.split(NUL_BYTE).join('') : value) as unknown as T;\n }\n if (Array.isArray(value)) {\n return value.map((item) => stripNulBytes(item)) as unknown as T;\n }\n if (value !== null && typeof value === 'object') {\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n return value;\n }\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, entry]) => [\n stripNulBytes(key),\n stripNulBytes(entry),\n ]),\n ) as unknown as T;\n }\n return value;\n}\n\n/**\n * A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the\n * `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call\n * {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index — or copy\n * {@link PgVectorStore.schemaStatements} into a migration.\n *\n * The constructor takes a `pg` `Pool`, a Drizzle database, a `postgres.js` `sql`, or a hand-written\n * {@link PgClient} (see {@link toPgClient}).\n *\n * This class does **not** implement the optional\n * {@link import('./vector-store.js').LexicalVectorStore} capability; {@link PgLexicalVectorStore}\n * (below) does. The split is deliberate: lexical search over Postgres needs a GIN index, and a\n * store that silently gained `searchText` on upgrade would be picked up by every\n * `isLexicalVectorStore(store)` check and start sequentially scanning an unindexed chunks table:\n *\n * - it is new DDL, and `CREATE INDEX` (non-concurrently, as `ensureSchema` must, since\n * `CONCURRENTLY` cannot run in a transaction) takes a write lock on an already-populated chunks\n * table — a boot-time stall proportional to corpus size for every existing deployment;\n * - it forces a text-search-configuration choice (`english` vs `simple` vs …) that must match between\n * the index expression and every query, or Postgres silently ignores the index and sequentially\n * scans the corpus — a failure mode that stays *correct* while quietly getting slower with scale.\n *\n * So lexical search is something you opt into by constructing the subclass, with the index created\n * by a migration you run and watch (or by `ensureSchema`, on a table small enough not to care).\n *\n * The enumeration and bulk-deletion methods {@link VectorStore} requires need nothing new here,\n * unlike the lexical capability above: enumeration and bulk deletion are `DISTINCT`, `= ANY($1)`, `DELETE … WHERE`\n * and `count(*)` over the table and index that already exist. No DDL, no boot-time lock, no migration.\n *\n * Subclassing: every statement filters through {@link PgVectorStore.whereConditions}, so a subclass\n * can map an opaque filter onto its own columns (a tenant id, an ACL) in one place.\n */\nexport class PgVectorStore implements VectorStore {\n protected readonly client: PgClient;\n protected readonly table: string;\n /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */\n private readonly dimensions: number | undefined;\n /** The widths with a partial HNSW index, for a mixed-dimension table. */\n private readonly indexedDimensions: readonly number[];\n private readonly nullableEmbeddings: boolean;\n private readonly iterativeScan: PgVectorStoreOptions['iterativeScan'];\n private readonly efSearch: number | undefined;\n private readonly upsertBatchSize: number;\n private iterativeScanSupport: Promise<boolean> | undefined;\n\n constructor(client: PgClientSource, options: PgVectorStoreOptions = {}) {\n this.client = toPgClient(client);\n this.table = options.table ?? 'agent_rag_chunks';\n const dimensions = options.dimensions ?? 1536;\n if (typeof dimensions === 'number') {\n this.dimensions = dimensions;\n this.indexedDimensions = [];\n } else {\n for (const width of dimensions) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);\n }\n }\n this.dimensions = undefined;\n this.indexedDimensions = [...new Set(dimensions)];\n }\n this.nullableEmbeddings = options.nullableEmbeddings ?? false;\n this.iterativeScan = options.iterativeScan;\n this.efSearch = options.efSearch;\n this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));\n }\n\n /** Telemetry self-description — the table is the namespace a retrieval was served from. */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'pg', collection: this.table };\n }\n\n /**\n * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead\n * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.\n */\n schemaStatements(): string[] {\n const mixed = this.dimensions === undefined;\n const column = mixed ? 'vector' : `vector(${this.dimensions})`;\n const statements = [\n 'CREATE EXTENSION IF NOT EXISTS vector',\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n source TEXT,\n metadata JSONB,\n embedding ${column}${this.nullableEmbeddings ? '' : ' NOT NULL'}\n )`,\n ];\n if (mixed) {\n for (const width of this.indexedDimensions) {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx\n ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)\n WHERE vector_dims(embedding) = ${width}`,\n );\n }\n } else {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx\n ON ${this.table} USING hnsw (embedding vector_cosine_ops)`,\n );\n }\n return statements;\n }\n\n /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */\n async ensureSchema(): Promise<void> {\n for (const statement of this.schemaStatements()) {\n await this.client.query(statement);\n }\n }\n\n /**\n * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}\n * rows per statement. An empty `embedding` is written as `NULL` (see\n * {@link PgVectorStoreOptions.nullableEmbeddings}).\n */\n async upsert(records: VectorRecord[]): Promise<void> {\n // Postgres rejects the NUL byte in text/jsonb — see stripNulBytes. The id is stripped before\n // de-duplication, so two ids that only differ by a NUL byte are the same row, as they'd be stored.\n const byId = new Map<string, VectorRecord>();\n for (const record of records) {\n const id = stripNulBytes(record.id);\n // Delete first so the surviving record takes the position of its LAST occurrence.\n byId.delete(id);\n byId.set(id, { ...record, id });\n }\n // One statement can't touch a row twice (`ON CONFLICT DO UPDATE command cannot affect row a\n // second time`), hence the de-duplication above: last write wins, as with the row-by-row loop.\n const unique = [...byId.values()];\n for (let start = 0; start < unique.length; start += this.upsertBatchSize) {\n const batch = unique.slice(start, start + this.upsertBatchSize);\n const params: unknown[] = [];\n const rows = batch.map((record) => {\n const text = stripNulBytes(record.text);\n const source = record.source !== undefined ? stripNulBytes(record.source) : undefined;\n const metadata = record.metadata !== undefined ? stripNulBytes(record.metadata) : undefined;\n const base = params.length;\n params.push(\n record.id,\n text,\n source ?? null,\n metadata !== undefined ? JSON.stringify(metadata) : null,\n record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null,\n );\n return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;\n });\n await this.client.query(\n `INSERT INTO ${this.table} (id, text, source, metadata, embedding)\n VALUES ${rows.join(', ')}\n ON CONFLICT (id) DO UPDATE SET\n text = EXCLUDED.text,\n source = EXCLUDED.source,\n metadata = EXCLUDED.metadata,\n embedding = EXCLUDED.embedding`,\n params,\n );\n }\n }\n\n async remove(documentId: string): Promise<void> {\n // Stripped so it matches the id `upsert` actually stored, and so a NUL-bearing id can't 0x00\n // the DELETE itself.\n await this.client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [\n stripNulBytes(documentId),\n ]);\n }\n\n /**\n * Rewrite a document's metadata without re-embedding it. See {@link VectorStore.updateMetadata}\n * for the semantics; here the whole thing is one statement, because jsonb already *is* the merge:\n * `||` is a shallow key-wise merge (right side wins, arrays replaced wholesale — precisely\n * {@link MetadataPatch}) and `- text[]` drops the keys the patch removed. `COALESCE` covers a chunk\n * ingested with no metadata at all, so a patch can create the object rather than no-op on `NULL`.\n *\n * Unlike RediSearch there is no second representation to keep in step — the `metadata` column is\n * both what `search` filters on and what it returns — so the correctness trap the Redis adapter has\n * to work for simply does not exist here. `RETURNING id` is what supplies the chunk count, since\n * {@link PgClient} is a rows-only surface with no `rowCount`.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n // Postgres rejects the NUL byte in text/jsonb — see stripNulBytes. `documentId` too, so it\n // matches the id `upsert` actually stored.\n const { set, remove } = splitMetadataPatch(stripNulBytes(patch));\n const rows = await this.client.query<{ id: string }>(\n `UPDATE ${this.table}\n SET metadata = (COALESCE(metadata, '{}'::jsonb) || $2::jsonb) - $3::text[]\n WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1\n RETURNING id`,\n [stripNulBytes(documentId), JSON.stringify(set), remove],\n );\n return rows.length;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const params: unknown[] = [];\n const where = this.where(filter, params);\n // DISTINCT ON collapses chunks to one row per document; all chunks share the doc's metadata.\n const rows = await this.client.query<{\n doc_id: string;\n metadata: Record<string, unknown> | null;\n }>(\n `SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata\n FROM ${this.table}\n ${where}\n ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,\n params,\n );\n return rows.map((row) => ({\n id: row.doc_id,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain\n * `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the\n * trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which\n * carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two\n * adapters.\n *\n * `embedding` is left out of the projection: it is the widest column in the table and useless to a\n * caller reading text back.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n // Stripped so it matches the id `upsert` actually stored.\n const params: unknown[] = [stripNulBytes(documentId)];\n let sql = `SELECT id, ${CHUNK_INDEX_FROM_ID} AS chunk_index, text, metadata\n FROM ${this.table}\n WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1\n ORDER BY chunk_index`;\n if (options?.limit !== undefined) {\n params.push(options.limit);\n sql += ` LIMIT $${params.length}`;\n }\n if (options?.offset !== undefined) {\n params.push(options.offset);\n sql += ` OFFSET $${params.length}`;\n }\n const rows = await this.client.query<{\n id: string;\n chunk_index: number;\n text: string;\n metadata: Record<string, unknown> | null;\n }>(sql, params);\n return rows.map((row) => ({\n id: row.id,\n index: Number(row.chunk_index),\n text: row.text,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n /** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ doc_id: string }>(\n `SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id\n FROM ${this.table}\n ${where}\n ORDER BY doc_id`,\n params,\n );\n return rows.map((row) => row.doc_id);\n }\n\n /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n // Stripped so every id matches what `upsert` actually stored.\n await this.client.query(\n `DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = ANY($1::text[])`,\n [stripNulBytes(documentIds)],\n );\n }\n\n /**\n * See {@link VectorStore.removeWhere}: empty filter object throws, empty-array value\n * deletes nothing. The count comes from `RETURNING id` rather than a driver-specific `rowCount`,\n * because {@link PgClient} is deliberately just \"run SQL, get rows\".\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n if (Object.keys(filter).length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the table. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n // Redundant with buildWhere's `false` clause, and stated anyway — the deny primitive should be\n // visible at the destructive call site, not one indirection away.\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ id: string }>(\n `DELETE FROM ${this.table} ${where} RETURNING id`,\n params,\n );\n return rows.length;\n }\n\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ count: number | string }>(\n `SELECT count(*)::int AS count FROM ${this.table} ${where}`,\n params,\n );\n return Number(rows[0]?.count ?? 0);\n }\n\n /**\n * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query\n * are compared (through that width's partial index when it has one); chunks without an embedding\n * are never returned.\n */\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (embedding.length === 0) {\n return [];\n }\n // $1 = query vector, $2 = topK; metadata-filter params start at $3.\n const params: unknown[] = [toVectorLiteral(embedding), options.topK];\n const conditions = this.whereConditions(options.filter, params);\n let vectorType = 'vector';\n if (this.dimensions === undefined) {\n const width = embedding.length;\n if (this.indexedDimensions.includes(width)) {\n vectorType = `vector(${width})`;\n }\n conditions.push('embedding IS NOT NULL', `vector_dims(embedding) = ${width}`);\n } else if (this.nullableEmbeddings) {\n conditions.push('embedding IS NOT NULL');\n }\n // A fixed-width table keeps the exact expression its index was built on (`embedding`); a\n // mixed one casts both sides so the planner can match the per-width partial index.\n const column = vectorType === 'vector' ? 'embedding' : `(embedding::${vectorType})`;\n const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score\n FROM ${this.table}\n ${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}\n ORDER BY ${column} <=> $1::${vectorType}\n LIMIT $2`;\n const rows = await this.withSearchSettings((client) => client.query<PgRow>(sql, params));\n return rows.map(toPassage);\n }\n\n /**\n * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`\n * (placeholders are numbered after what `params` already holds). Every statement that takes a\n * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,\n * an access list — overrides this one method. Return `['false']` to deny.\n */\n protected whereConditions(\n filter: Record<string, unknown> | undefined,\n params: unknown[],\n ): string[] {\n return buildWhere(filter, params);\n }\n\n private where(filter: Record<string, unknown> | undefined, params: unknown[]): string {\n const conditions = this.whereConditions(filter, params);\n return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n }\n\n /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */\n private async withSearchSettings<T>(read: (client: PgClient) => Promise<T>): Promise<T> {\n const transaction = this.client.transaction?.bind(this.client);\n if (transaction === undefined) {\n return read(this.client);\n }\n const iterative = this.iterativeScan !== undefined && (await this.supportsIterativeScan());\n if (!iterative && this.efSearch === undefined) {\n return read(this.client);\n }\n return transaction(async (tx) => {\n if (iterative) {\n await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [this.iterativeScan]);\n }\n if (this.efSearch !== undefined) {\n await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [String(this.efSearch)]);\n }\n return read(tx);\n });\n }\n\n /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */\n private supportsIterativeScan(): Promise<boolean> {\n this.iterativeScanSupport ??= this.client\n .query<{ extversion: string }>(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`)\n .then((rows) => {\n const [major = 0, minor = 0] = (rows[0]?.extversion ?? '0.0').split('.').map(Number);\n return major > 0 || minor >= 8;\n })\n .catch(() => false);\n return this.iterativeScanSupport;\n }\n}\n\n/**\n * {@link PgVectorStore} plus Postgres full-text search: a {@link LexicalVectorStore}, so\n * `LexicalRetriever` and `HybridRetriever` work on Postgres exactly as on RediSearch — the dense leg\n * over pgvector, the lexical leg over a `tsvector`, both on the same rows and ids.\n *\n * A question is searched by its meaningful terms: its stop words dropped, rows holding any remaining\n * term ranked by IDF-weighted coverage, `ts_rank_cd` (cover density) breaking ties (see\n * {@link PgFullTextOptions.anyTermFallback}); explicit syntax (quoted phrases, `-exclusions`) goes\n * through `websearch_to_tsquery` first.\n *\n * The `tsvector` is either a column you own ({@link PgFullTextOptions.column}) or the expression\n * `to_tsvector(config, text)`, for which {@link PgVectorStore.schemaStatements} adds a GIN\n * expression index. See {@link PgVectorStore} for why that index belongs in a migration on a\n * populated table.\n */\nexport class PgLexicalVectorStore extends PgVectorStore implements LexicalVectorStore {\n private readonly tsConfig: string;\n private readonly tsColumn: string | undefined;\n private readonly anyTermFallback: boolean;\n private readonly stopWords: Readonly<Record<string, ReadonlySet<string>>>;\n\n constructor(\n client: PgClientSource,\n options: PgVectorStoreOptions & { fullText?: PgFullTextOptions } = {},\n ) {\n super(client, options);\n const config = options.fullText?.config ?? 'simple';\n if (!SQL_IDENTIFIER.test(config)) {\n throw new Error(`PgLexicalVectorStore: invalid text search configuration \"${config}\"`);\n }\n const column = options.fullText?.column;\n if (column !== undefined && !SQL_IDENTIFIER.test(column)) {\n throw new Error(`PgLexicalVectorStore: invalid tsvector column \"${column}\"`);\n }\n this.tsConfig = config;\n this.tsColumn = column;\n this.anyTermFallback = options.fullText?.anyTermFallback ?? true;\n const stopWords = options.fullText?.stopWords;\n this.stopWords = stopWords === false ? {} : (stopWords ?? DEFAULT_STOP_WORDS);\n }\n\n override schemaStatements(): string[] {\n const statements = super.schemaStatements();\n if (this.tsColumn === undefined) {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx\n ON ${this.table} USING gin (${this.tsvector()})`,\n );\n }\n return statements;\n }\n\n async searchText(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter) || query.trim() === '') {\n return [];\n }\n if (!this.anyTermFallback || hasSearchSyntax(query)) {\n const strict = await this.runTextSearch(query, options);\n if (strict.length > 0 || !this.anyTermFallback) {\n return strict;\n }\n }\n const terms = keywordTerms(query, 24, this.stopWords);\n return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);\n }\n\n /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */\n private async runTextSearch(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n const tsquery = `websearch_to_tsquery('${this.tsConfig}', $1)`;\n // $1 = query, $2 = topK; metadata-filter params start at $3.\n const params: unknown[] = [query, options.topK];\n const conditions = [\n `${this.tsvector()} @@ ${tsquery}`,\n ...this.whereConditions(options.filter, params),\n ];\n const rows = await this.client.query<PgRow>(\n `SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score\n FROM ${this.table}\n WHERE ${conditions.join(' AND ')}\n ORDER BY score DESC, id\n LIMIT $2`,\n params,\n );\n return rows.map(toPassage);\n }\n\n /**\n * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,\n * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated\n * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full\n * of common words can match most of the table.\n */\n private async runRankedTermSearch(\n terms: string[],\n options: VectorSearchOptions,\n ): Promise<Passage[]> {\n const any = `to_tsquery('${this.tsConfig}', $1)`;\n // $1 = any-term tsquery, $2 = topK, $3 = terms; metadata-filter params start at $4.\n // Terms are letters/digits only, so quoting each one is enough to make it a literal lexeme.\n const params: unknown[] = [anyTermTsquery(terms), options.topK, terms];\n const conditions = [\n `${this.tsvector()} @@ ${any}`,\n ...this.whereConditions(options.filter, params),\n ];\n const rows = await this.client.query<PgRow>(\n `WITH __terms AS (\n SELECT DISTINCT to_tsquery('${this.tsConfig}', quote_literal(t)) AS q\n FROM unnest($3::text[]) AS t\n ), __cand AS MATERIALIZED (\n SELECT id, text, source, metadata, ${this.tsvector()} AS __tsv\n FROM ${this.table}\n WHERE ${conditions.join(' AND ')}\n ), __hits AS MATERIALIZED (\n SELECT __cand.id AS __id, __terms.q FROM __cand JOIN __terms ON __cand.__tsv @@ __terms.q\n ), __idf AS (\n SELECT q, ln(1 + ((SELECT count(*) FROM __cand) - count(*) + 0.5) / (count(*) + 0.5)) AS w\n FROM __hits GROUP BY q\n ), __scored AS (\n SELECT __hits.__id, sum(__idf.w) AS s, rank() OVER (ORDER BY sum(__idf.w) DESC) AS r\n FROM __hits JOIN __idf USING (q) GROUP BY __hits.__id\n )\n SELECT __cand.id, __cand.text, __cand.source, __cand.metadata,\n __scored.s + ts_rank_cd(__cand.__tsv, ${any}, 32) AS score\n FROM __scored JOIN __cand ON __cand.id = __scored.__id\n WHERE __scored.r <= $2\n ORDER BY __scored.s DESC, ts_rank_cd(__cand.__tsv, ${any}, 32) DESC, __cand.id\n LIMIT $2`,\n params,\n );\n return rows.map(toPassage);\n }\n\n /** Must be character-for-character the index expression, or the planner won't use the index. */\n private tsvector(): string {\n return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;\n }\n}\n\nconst SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/;\n\ninterface PgRow {\n id: string;\n text: string;\n source: string | null;\n metadata: Record<string, unknown> | null;\n score: number | string;\n}\n\nfunction toPassage(row: PgRow): Passage {\n return {\n id: row.id,\n text: row.text,\n score: Number(row.score),\n ...(row.source !== null ? { source: row.source } : {}),\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n };\n}\n\n/**\n * Build the conditions for a metadata `filter`, appending their bindings to `params` (placeholders\n * continue from `params.length`). Scalar values collapse into a single `@>` jsonb-containment check\n * (exact-match, as before). An **array** value is a **match-any** (OR / set membership) check via\n * jsonb `?|`: the record matches when its value for that key — scalar or array — shares an element\n * with the filter array. An empty array can never match (deny primitive). Keys are passed as\n * parameters (`metadata->$k`) so a caller-supplied metadata key can't inject SQL. Returns `[]` when\n * there is no filter, preserving the previous unfiltered query shape.\n *\n * The filter is run through {@link stripNulBytes} up front: both the keys (bound as `$k` params) and\n * the scalar/array values end up as query bindings, and a caller-supplied filter is exactly as\n * capable of carrying a NUL byte as the text it is filtering.\n */\nfunction buildWhere(filter: Record<string, unknown> | undefined, params: unknown[]): string[] {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return [];\n }\n const stripped = stripNulBytes(filter);\n const clauses: string[] = [];\n const scalar: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(stripped)) {\n if (Array.isArray(value)) {\n if (value.length === 0) {\n clauses.push('false');\n continue;\n }\n params.push(key);\n const keyParam = `$${params.length}`;\n params.push(value.map(String));\n const arrParam = `$${params.length}`;\n clauses.push(\n `(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' ` +\n `THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`,\n );\n } else {\n scalar[key] = value;\n }\n }\n if (Object.keys(scalar).length > 0) {\n params.push(JSON.stringify(scalar));\n clauses.push(`metadata @> $${params.length}::jsonb`);\n }\n return clauses;\n}\n\n/**\n * SQL that collapses a chunk id (`${documentId}#<n>`) back to its source document id — the pgvector\n * mirror of {@link import('./vector-store.js').documentIdOf}. Shared by `remove` and\n * `listDocuments` so both key on the exact same definition of \"chunk belongs to document\".\n */\nconst DOCUMENT_ID_FROM_CHUNK = \"regexp_replace(id, '#[0-9]+$', '')\";\n\n/** The `n` of `…#<n>` as an integer, so chunks sort numerically; a bare id (no suffix) is `0`. */\nconst CHUNK_INDEX_FROM_ID = \"COALESCE((substring(id from '#([0-9]+)$'))::int, 0)\";\n\n/** pgvector accepts a `'[1,2,3]'` text literal cast to `vector`. */\nfunction toVectorLiteral(embedding: number[]): string {\n return `[${embedding.join(',')}]`;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing } from './filter.js';\nimport { type MetadataPatch, applyMetadataPatch, isEmptyMetadataPatch } from './metadata-patch.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport {\n type IndexedDocument,\n type LexicalVectorStore,\n type ListChunksOptions,\n type StoredChunk,\n UnsafeRemovalError,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n chunkIndexOf,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * The minimal Redis surface {@link RedisVectorStore} needs: send a raw command and get the reply.\n * Both `node-redis` (`client.sendCommand(args)`) and `ioredis` (wrap `redis.call`) satisfy it — so\n * this package pulls in NO Redis driver (bring your own, like the other adapters). Requires the\n * RediSearch module (Redis Stack / Redis 8+).\n */\nexport interface RedisSearchClient {\n sendCommand(args: (string | Buffer)[]): Promise<unknown>;\n}\n\n/**\n * The index that already exists in Redis contradicts this store's configuration in a way that cannot\n * be repaired in place. Thrown by {@link RedisVectorStore.ensureSchema} — which would otherwise have\n * returned happily and left every subsequent query quietly wrong.\n *\n * The canonical case is a changed embedding width (a model swap, 1024 → 1536): RediSearch fixes a\n * vector field's `DIM` at `FT.CREATE`, and every vector already stored was produced by the OLD model,\n * so there is no in-place fix — the index has to be dropped and the corpus re-embedded. That is\n * destructive and belongs to the host, never to a boot-time `ensureSchema`.\n */\nexport class RedisVectorSchemaMismatchError extends Error {\n constructor(\n /** The RediSearch index that was inspected. */\n readonly index: string,\n /** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */\n readonly field: string,\n /** What this store is configured for. */\n readonly expected: string,\n /** What `FT.INFO` reports the live index has. */\n readonly actual: string,\n remedy: string,\n ) {\n super(\n `RediSearch index \"${index}\" disagrees with this RedisVectorStore's configuration: ` +\n `field \"${field}\" is ${actual}, expected ${expected}. ${remedy}`,\n );\n this.name = 'RedisVectorSchemaMismatchError';\n }\n}\n\nexport interface RedisVectorStoreOptions {\n /** RediSearch index name. Default `agent_rag_idx`. */\n index?: string;\n /** Key prefix for the per-chunk hashes. Default `agent_rag:`. */\n prefix?: string;\n /** Embedding width — must match your model (e.g. 1536). Default 1536. */\n dimensions?: number;\n /**\n * Metadata keys to index as filterable TAG fields. RediSearch needs filter fields declared up\n * front, so only keys listed here can be passed in `search`'s `filter`. Default none.\n */\n filterableFields?: string[];\n /**\n * RediSearch scorer used by {@link RedisVectorStore.searchText}. Default `BM25` — the standard\n * lexical ranking, and the same family the in-process `KeywordRetriever` implements. Override for\n * a server that names it differently (`BM25STD` on newer builds) or to pick `TFIDF`/`DISMAX`.\n */\n lexicalScorer?: string;\n}\n\n/**\n * A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option\n * for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the\n * embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}\n * once at boot to create the index.\n *\n * The chunk text is declared as a RediSearch `TEXT` field, so this store also satisfies\n * {@link LexicalVectorStore}: {@link RedisVectorStore.searchText} is BM25 over that same index —\n * the lexical half of hybrid search with no second index to build, feed, or invalidate.\n */\nexport class RedisVectorStore implements LexicalVectorStore {\n private readonly index: string;\n private readonly prefix: string;\n private readonly dimensions: number;\n private readonly filterableFields: string[];\n private readonly lexicalScorer: string;\n\n constructor(\n private readonly client: RedisSearchClient,\n options: RedisVectorStoreOptions = {},\n ) {\n this.index = options.index ?? 'agent_rag_idx';\n this.prefix = options.prefix ?? 'agent_rag:';\n this.dimensions = options.dimensions ?? 1536;\n this.filterableFields = options.filterableFields ?? [];\n this.lexicalScorer = options.lexicalScorer ?? 'BM25';\n }\n\n /**\n * Telemetry self-description — the RediSearch INDEX, not the key prefix: the index is what both\n * legs query, and two stores sharing one index (the dense and lexical halves of a hybrid) must\n * report the same namespace or `describeSharedSource` would see them disagree and drop the store.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'redis', collection: this.index };\n }\n\n /**\n * Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —\n * and if it DOES exist, compare what's there against this store's configuration instead of\n * assuming they agree. An index that outlives a config change is the normal case (schemas live in\n * Redis, config lives in a redeployed process), and the two failure modes it produces are both\n * invisible at runtime: a `filterableFields` entry added later has no `meta_*` TAG, so every search\n * filtering on it matches *nothing*; a changed `dimensions` leaves the index on the old width, so\n * writes are rejected or KNN ranks garbage. Neither raises anything on its own.\n *\n * The two are repaired differently, because only one of them CAN be repaired:\n *\n * - **A missing filterable TAG is additive** → `FT.ALTER … SCHEMA ADD`. Nothing that already works\n * changes, so doing it automatically is safe. Note the chunks written *before* the alter carry no\n * `meta_*` hash field at all (`upsert` only writes the fields configured at the time), so they\n * only become filterable on that key once they're re-ingested — the schema is repaired here, the\n * backfill is the host's call.\n * - **A dimension (or field-type) change is not repairable** → {@link RedisVectorSchemaMismatchError}.\n * It needs a drop + full reindex, which would destroy the host's corpus; failing loudly at boot is\n * the only honest option.\n *\n * If `FT.INFO` comes back in a shape this parser doesn't recognise, no drift is inferred — an\n * unreadable reply must not be turned into a false alarm that blocks a boot.\n */\n async ensureSchema(): Promise<void> {\n let info: unknown;\n try {\n info = await this.client.sendCommand(['FT.INFO', this.index]);\n } catch {\n await this.createIndex();\n return;\n }\n await this.reconcileSchema(info);\n }\n\n /** Repair what can be repaired in place; throw on what can't. */\n private async reconcileSchema(info: unknown): Promise<void> {\n const attributes = parseIndexAttributes(info);\n if (attributes.size === 0) {\n return;\n }\n\n const vector = attributes.get('embedding');\n if (vector?.dimensions !== undefined && vector.dimensions !== this.dimensions) {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n 'embedding',\n `DIM ${this.dimensions}`,\n `DIM ${vector.dimensions}`,\n `A vector field's width is fixed at FT.CREATE and every stored embedding has the old one, so this cannot be altered in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest with the new embedding model.`,\n );\n }\n\n const missing: string[] = [];\n for (const field of this.filterableFields) {\n const name = `meta_${field}`;\n const existing = attributes.get(name);\n if (existing === undefined) {\n missing.push(name);\n } else if (existing.type !== 'TAG') {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n name,\n 'TAG',\n existing.type,\n `A field's type cannot be changed in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest, or rename the metadata key.`,\n );\n }\n }\n for (const name of missing) {\n await this.client.sendCommand(['FT.ALTER', this.index, 'SCHEMA', 'ADD', name, 'TAG']);\n }\n }\n\n private async createIndex(): Promise<void> {\n const schema: string[] = ['text', 'TEXT', 'source', 'TAG', 'metadata_json', 'TEXT', 'NOINDEX'];\n for (const field of this.filterableFields) {\n schema.push(`meta_${field}`, 'TAG');\n }\n schema.push(\n 'embedding',\n 'VECTOR',\n 'HNSW',\n '6',\n 'TYPE',\n 'FLOAT32',\n 'DIM',\n String(this.dimensions),\n 'DISTANCE_METRIC',\n 'COSINE',\n );\n await this.client.sendCommand([\n 'FT.CREATE',\n this.index,\n 'ON',\n 'HASH',\n 'PREFIX',\n '1',\n this.prefix,\n 'SCHEMA',\n ...schema,\n ]);\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n const args: (string | Buffer)[] = [\n 'HSET',\n `${this.prefix}${record.id}`,\n 'text',\n record.text,\n 'embedding',\n encodeVector(record.embedding),\n ];\n if (record.source !== undefined) {\n args.push('source', record.source);\n }\n if (record.metadata !== undefined) {\n args.push('metadata_json', JSON.stringify(record.metadata));\n for (const field of this.filterableFields) {\n const value = record.metadata[field];\n if (value !== undefined) {\n args.push(`meta_${field}`, encodeTag(value));\n }\n }\n }\n await this.client.sendCommand(args);\n }\n }\n\n /**\n * Single-document deletion, by `SCAN … MATCH`.\n *\n * This deliberately stays a keyspace scan even though the rest of this class no longer needs one.\n * The obvious optimisation is to stamp each chunk with its document id as an implicit filterable\n * TAG at {@link RedisVectorStore.upsert} and turn `remove` into an `FT.SEARCH` — and that is exactly\n * the change that must not ship, because chunks written *before* it carry no such field. `FT.SEARCH`\n * would find zero of them, `remove` would return happily, and a document that has been \"deleted\"\n * would go on being retrieved. A delete that silently stops deleting is worse than a slow one, and\n * the library cannot tell a fully-migrated corpus from an unmigrated one: both look like \"this\n * document has no indexed chunks\". Nor is a search-then-fall-back-to-scan hybrid safe — a document\n * re-ingested after the upgrade has *some* stamped chunks, so the search finds hits, the fallback\n * never runs, and the unstamped tail survives. That is the same silent failure with a smaller blast\n * radius, which makes it harder to notice rather than less wrong.\n *\n * The consumer pain this was measured against — deleting a whole collection, one `remove` per\n * document — is answered instead by {@link RedisVectorStore.removeWhere} (one filtered query, no\n * keyspace scan at all) and {@link RedisVectorStore.removeMany} (one scan for N documents rather\n * than N scans). Neither depends on a field that older chunks lack, so neither has a migration.\n */\n async remove(documentId: string): Promise<void> {\n const keys = await this.chunkKeys(documentId);\n if (keys.length > 0) {\n await this.client.sendCommand(['DEL', ...keys]);\n }\n }\n\n /**\n * Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`\n * alone. See {@link VectorStore.updateMetadata} for the merge semantics and the return value.\n *\n * The care this needs is entirely about the fact that a chunk stores its metadata **twice**: as the\n * `metadata_json` blob (what {@link RedisVectorStore.search} reads back onto a `Passage`) and as\n * `meta_<field>` TAG fields (what RediSearch actually filters on, and only for the declared\n * `filterableFields`). Move one without the other and the store lies to itself — a chunk that\n * filters as the old value but reports the new one, or the reverse — so this method rewrites both\n * from the same merged object:\n *\n * - a patched key that is **not** filterable lands in `metadata_json` only, and no stray `meta_*`\n * field is invented for it (it would be dead weight the index does not know about);\n * - a patched key that **is** filterable also rewrites its TAG, using the identical encoding\n * `upsert` uses, so an updated chunk is indistinguishable from a re-ingested one;\n * - a filterable key the patch **removes** has its TAG `HDEL`-ed, not left behind as a tag matching\n * a value the document no longer carries.\n *\n * A plain `HSET` is all the index needs: RediSearch re-indexes a hash the moment it is written, so\n * the very next `FT.SEARCH` sees the new TAG and no longer the old one — no reindex command, and\n * no need to rewrite the (untouched, and expensive) vector to nudge it. `HDEL` on an indexed field\n * likewise drops it from the index. Verified against a real Redis Stack in\n * `redis-vector-store.db.spec.ts`, not assumed.\n *\n * Removals are issued **before** the write, so the only window a concurrent reader can observe is\n * \"TAG already gone, `metadata_json` not yet updated\" — which fails *closed* on a filter carrying\n * an ACL. The reverse order would briefly leave a document tagged with a capability its metadata no\n * longer claims. Read-modify-write across chunks is not transactional; concurrent writers to the\n * same chunk are last-writer-wins, exactly as two concurrent `upsert`s already are.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n const keys = await this.chunkKeys(documentId);\n let updated = 0;\n for (const key of keys) {\n const raw = await this.client.sendCommand(['HGET', key, 'metadata_json']);\n const current = parseMetadata(raw === null || raw === undefined ? undefined : toStr(raw));\n const next = applyMetadataPatch(current.metadata, patch);\n\n const args: (string | Buffer)[] = ['HSET', key, 'metadata_json', JSON.stringify(next)];\n const stale: string[] = [];\n for (const field of this.filterableFields) {\n const value = next[field];\n if (value !== undefined) {\n args.push(`meta_${field}`, encodeTag(value));\n } else if (current.metadata?.[field] !== undefined) {\n stale.push(`meta_${field}`);\n }\n }\n if (stale.length > 0) {\n await this.client.sendCommand(['HDEL', key, ...stale]);\n }\n await this.client.sendCommand(args);\n updated += 1;\n }\n return updated;\n }\n\n /**\n * Every chunk key that currently EXISTS for `documentId` — the bare `${prefix}${documentId}` plus\n * the `${documentId}#<n>` chunks found by `SCAN`. One definition of \"chunk belongs to document\",\n * shared by `remove` and `updateMetadata`, so the two can never disagree about which keys a\n * document owns. The glob is escaped so `gone` does not reach into `goner#0`.\n */\n private async chunkKeys(documentId: string): Promise<string[]> {\n const keys = new Set<string>();\n const bare = `${this.prefix}${documentId}`;\n const exists = await this.client.sendCommand(['EXISTS', bare]);\n if (Number(toStr(exists)) > 0) {\n keys.add(bare);\n }\n const pattern = `${this.prefix}${escapeGlob(documentId)}#*`;\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n pattern,\n 'COUNT',\n '256',\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n keys.add(key);\n }\n } while (cursor !== '0');\n return [...keys];\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.\n * Two reasons, and the second is the one that matters: the query language has no way to ask for\n * \"every chunk of this document\" — `id` is the key name, not an indexed field — and `FT.SEARCH`\n * caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does\n * not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and\n * {@link RedisVectorStore.updateMetadata} already use, so \"the chunks this returns\" and \"the chunks\n * a delete would take\" cannot drift apart.\n *\n * It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this\n * is an inspection path (one document, on demand) and not something retrieval runs — but it is why\n * this must not be put on a hot path or called in a loop over a corpus.\n *\n * `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing\n * a caller reading text back can do with it.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n const keys = await this.chunkKeys(documentId);\n const ordered = keys\n .map((key) => ({ key, id: key.slice(this.prefix.length) }))\n .map((entry) => ({ ...entry, index: chunkIndexOf(entry.id) }))\n .sort((a, b) => a.index - b.index);\n\n const offset = options?.offset ?? 0;\n const page =\n options?.limit === undefined\n ? ordered.slice(offset)\n : ordered.slice(offset, offset + options.limit);\n\n const chunks: StoredChunk[] = [];\n for (const entry of page) {\n const reply = await this.client.sendCommand(['HMGET', entry.key, 'text', 'metadata_json']);\n const fields = Array.isArray(reply) ? reply : [];\n const text = fields[0] === null || fields[0] === undefined ? undefined : toStr(fields[0]);\n if (text === undefined) {\n // The key was deleted between the SCAN and this read. Skip it rather than reporting a chunk\n // with empty text, which would read as \"the chunker produced nothing here\".\n continue;\n }\n const raw = fields[1] === null || fields[1] === undefined ? undefined : toStr(fields[1]);\n chunks.push({\n id: entry.id,\n index: entry.index,\n text,\n ...parseMetadata(raw),\n });\n }\n return chunks;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Map<string, IndexedDocument>();\n const batchSize = 1000;\n let offset = 0;\n for (;;) {\n // RETURN metadata_json (not NOCONTENT) so we can carry a representative chunk's metadata;\n // parseSearchReply already yields prefix-stripped ids + parsed metadata for both RESP2/RESP3.\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'RETURN',\n '1',\n 'metadata_json',\n 'LIMIT',\n String(offset),\n String(batchSize),\n 'DIALECT',\n '2',\n ]);\n const passages = parseSearchReply(reply, this.prefix);\n for (const passage of passages) {\n const id = documentIdOf(passage.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(passage.metadata !== undefined ? { metadata: passage.metadata } : {}),\n });\n }\n }\n if (passages.length < batchSize) {\n break;\n }\n offset += batchSize;\n }\n return [...documents.values()];\n }\n\n /**\n * The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:\n * RediSearch returns bare keys, so nothing carries `metadata_json` over the wire and nothing is\n * JSON-parsed. `listDocuments` has to parse one metadata blob per *chunk* — thousands of parses to\n * produce a few hundred ids it then throws the metadata away from — and this does none of it.\n *\n * The collapse to document ids still happens client-side. RediSearch can group server-side\n * (`FT.AGGREGATE … GROUPBY`), but only over an indexed *field*, and a chunk's document id lives in\n * its key, not in a field — deriving it would mean stamping one at write time, which is the\n * migration {@link RedisVectorStore.remove} explains this class refuses. Deduplicating a page of\n * keys in JS is free by comparison.\n */\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Set<string>();\n let offset = 0;\n for (;;) {\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'NOCONTENT',\n 'LIMIT',\n String(offset),\n String(SCAN_PAGE),\n 'DIALECT',\n '2',\n ]);\n const keys = parseSearchKeys(reply);\n for (const key of keys) {\n documents.add(documentIdOf(stripPrefix(key, this.prefix)));\n }\n if (keys.length < SCAN_PAGE) {\n break;\n }\n offset += SCAN_PAGE;\n }\n return [...documents];\n }\n\n /**\n * Bulk deletion by document id — **one** keyspace scan for N documents, where N calls to\n * {@link RedisVectorStore.remove} would be N of them. The scan is `MATCH ${prefix}*`, so Redis still\n * filters to this store's own keys server-side; membership is then decided in JS against a `Set`,\n * which is what lets a single pass serve any number of ids.\n *\n * It deletes by key, exactly as `remove` does, so it is correct on chunks written by any version of\n * this library — there is no field it needs the corpus to already carry.\n */\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n const wanted = new Set(documentIds);\n // A document may have been stored under its bare id (no `#n` suffix); those keys are exact and\n // need no scan to find, so seed them and let the scan add the chunked ones.\n let pending = documentIds.map((id) => `${this.prefix}${id}`);\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n `${escapeGlob(this.prefix)}*`,\n 'COUNT',\n String(SCAN_PAGE),\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n if (wanted.has(documentIdOf(stripPrefix(key, this.prefix)))) {\n pending.push(key);\n }\n }\n if (pending.length >= DELETE_BATCH) {\n await this.client.sendCommand(['DEL', ...pending]);\n pending = [];\n }\n } while (cursor !== '0');\n if (pending.length > 0) {\n await this.client.sendCommand(['DEL', ...pending]);\n }\n }\n\n /**\n * Delete every chunk matching `filter`, via `FT.SEARCH … NOCONTENT` + `DEL` — no keyspace scan at\n * all, so dropping a collection costs one indexed query per page instead of one full pass per\n * document. Resolves the number of chunk keys actually deleted.\n *\n * The two refusals, in this order, before any query is issued:\n *\n * 1. **An empty filter object throws** {@link UnsafeRemovalError}. `buildFilter({})` is `*`, and `*`\n * here would mean \"delete the corpus\" — far too destructive an outcome for the shape a filter\n * degenerates to when it is built wrong.\n * 2. **An empty-array value deletes nothing** and resolves `0`. This is the package's deny primitive\n * (an actor with no capability tokens), and it is the one that has to be gated *explicitly*: a\n * plausible-looking \"normalise the filter first\" refactor that drops empty arrays turns a deny\n * into `*` and wipes everything the deny existed to protect.\n *\n * Then one more, which is specific to RediSearch: every filter key must be a declared\n * `filterableFields` entry. `search` lets the engine reject an unknown field, which is fine for a\n * read; for a delete the check is worth making up front and by name.\n *\n * Deletion happens page by page, always re-querying from offset 0 — the rows just deleted are gone\n * from the index, so paging forward would skip their replacements. If a page comes back non-empty\n * but `DEL` reports nothing removed (keys expired between query and delete, or the index is stale),\n * the loop stops rather than spinning on results it can never consume.\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the index. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n const unindexed = keys.filter((key) => !this.filterableFields.includes(key));\n if (unindexed.length > 0) {\n const named = unindexed.map((key) => `\"${key}\"`).join(', ');\n const declared = this.filterableFields.join(', ') || 'none';\n throw new UnsafeRemovalError(\n 'unindexed-field',\n `removeWhere() cannot scope on ${named}: not declared in this store's filterableFields (${declared}), so RediSearch has no TAG to match on.`,\n );\n }\n // THE guard. Removing it turns the deny primitive into an unfiltered delete — see the db spec's\n // \"an empty-array filter deletes NOTHING\" case, which exists to fail loudly if this ever goes.\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n\n const query = buildFilter(filter);\n let removed = 0;\n for (;;) {\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'NOCONTENT',\n 'LIMIT',\n '0',\n String(DELETE_BATCH),\n 'DIALECT',\n '2',\n ]);\n const page = parseSearchKeys(reply);\n if (page.length === 0) {\n break;\n }\n const deleted = Number(await this.client.sendCommand(['DEL', ...page]));\n if (!Number.isFinite(deleted) || deleted === 0) {\n break;\n }\n removed += deleted;\n }\n return removed;\n }\n\n /**\n * Count matching chunks with `FT.SEARCH … LIMIT 0 0` — RediSearch returns the total and no\n * documents, so the cost is the query, not the corpus. An empty-array filter value denies and\n * yields `0`, same as every other filtered path here.\n */\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n buildFilter(filter),\n 'NOCONTENT',\n 'LIMIT',\n '0',\n '0',\n 'DIALECT',\n '2',\n ]);\n return parseSearchTotal(reply);\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const query = `${buildFilter(options.filter)}=>[KNN ${options.topK} @embedding $BLOB AS vector_score]`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'PARAMS',\n '2',\n 'BLOB',\n encodeVector(embedding),\n 'RETURN',\n '4',\n 'text',\n 'source',\n 'metadata_json',\n 'vector_score',\n 'SORTBY',\n 'vector_score',\n 'ASC',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix);\n }\n\n /**\n * Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of\n * hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the\n * corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as\n * `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector\n * hit would have respected.\n *\n * A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is\n * deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a\n * relevance query with nothing to be relevant to should do.\n */\n async searchText(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const terms = tokenizeQuery(query);\n if (terms.length === 0) {\n return [];\n }\n const filterQuery = buildFilter(options.filter);\n const textQuery = `@text:(${terms.join('|')})`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n filterQuery === '*' ? textQuery : `(${filterQuery} ${textQuery})`,\n 'SCORER',\n this.lexicalScorer,\n 'WITHSCORES',\n 'RETURN',\n '3',\n 'text',\n 'source',\n 'metadata_json',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix, 'lexical');\n }\n}\n\n/** Page size for `SCAN COUNT` and for the id-only `FT.SEARCH` pages. */\nconst SCAN_PAGE = 1000;\n\n/** How many keys to hand a single `DEL`, and therefore how big a `removeWhere` page is. */\nconst DELETE_BATCH = 512;\n\n/** Recover a chunk id from its Redis key. */\nfunction stripPrefix(key: string, prefix: string): string {\n return key.startsWith(prefix) ? key.slice(prefix.length) : key;\n}\n\n/**\n * Parse the keys out of an `FT.SEARCH … NOCONTENT` reply, tolerating both wire shapes: the RESP2 array\n * `[total, key, key, …]` and the RESP3 object `{ total_results, results: [{ id }] }`. Keys come back\n * whole (prefix included) — callers strip it.\n */\nfunction parseSearchKeys(reply: unknown): string[] {\n if (Array.isArray(reply)) {\n return reply.slice(1).map(toStr);\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n return Array.isArray(results) ? results.map(readId) : [];\n }\n return [];\n}\n\n/**\n * Parse the match count out of an `FT.SEARCH … LIMIT 0 0` reply — element 0 in RESP2, `total_results`\n * in the RESP3 object. An unrecognised shape counts as 0 rather than `NaN`, so a caller comparing\n * against a threshold can't be handed something that fails every comparison silently.\n */\nfunction parseSearchTotal(reply: unknown): number {\n if (Array.isArray(reply)) {\n const total = Number(toStr(reply[0]));\n return Number.isFinite(total) ? total : 0;\n }\n if (typeof reply === 'object' && reply !== null && 'total_results' in reply) {\n const total = Number((reply as { total_results: unknown }).total_results);\n return Number.isFinite(total) ? total : 0;\n }\n return 0;\n}\n\n/** One attribute as the live index reports it — enough to spot the drift that matters. */\ninterface IndexAttribute {\n /** `TEXT` / `TAG` / `VECTOR`. */\n type: string;\n /** `VECTOR` fields only: the width the index was created with. */\n dimensions?: number;\n}\n\n/**\n * Parse `FT.INFO`'s `attributes` into `attribute name → { type, dimensions }`, tolerating both wire\n * shapes exactly like {@link parseSearchReply} does: the RESP2 flat key/value array (whose attributes\n * are themselves flat key/value arrays) and node-redis's RESP3 object (attributes as objects).\n * Unknown shapes yield an empty map, which callers read as \"can't tell\" rather than \"nothing there\".\n */\nfunction parseIndexAttributes(reply: unknown): Map<string, IndexAttribute> {\n const attributes = new Map<string, IndexAttribute>();\n for (const entry of readInfoField(reply, 'attributes')) {\n const fields = readKeyValues(entry);\n const name = fields.attribute ?? fields.identifier;\n if (name === undefined) {\n continue;\n }\n const dim = Number(fields.dim);\n attributes.set(name, {\n type: (fields.type ?? '').toUpperCase(),\n ...(fields.dim !== undefined && Number.isFinite(dim) ? { dimensions: dim } : {}),\n });\n }\n return attributes;\n}\n\n/** Read one top-level `FT.INFO` field as an array, from either the RESP2 pair-array or RESP3 object. */\nfunction readInfoField(reply: unknown, key: string): unknown[] {\n if (Array.isArray(reply)) {\n for (let index = 0; index + 1 < reply.length; index += 2) {\n if (toStr(reply[index]) === key) {\n const value = reply[index + 1];\n return Array.isArray(value) ? value : [];\n }\n }\n return [];\n }\n if (typeof reply === 'object' && reply !== null && key in reply) {\n const value = (reply as Record<string, unknown>)[key];\n return Array.isArray(value) ? value : [];\n }\n return [];\n}\n\n/** Flatten one attribute entry (RESP2 pair-array or RESP3 object) into lower-cased string fields. */\nfunction readKeyValues(entry: unknown): Record<string, string | undefined> {\n const fields: Record<string, string | undefined> = {};\n if (Array.isArray(entry)) {\n for (let index = 0; index + 1 < entry.length; index += 2) {\n fields[toStr(entry[index]).toLowerCase()] = toStr(entry[index + 1]);\n }\n return fields;\n }\n if (typeof entry === 'object' && entry !== null) {\n for (const [key, value] of Object.entries(entry)) {\n fields[key.toLowerCase()] = toStr(value);\n }\n }\n return fields;\n}\n\n/** RediSearch expects a little-endian FLOAT32 buffer for the query/stored vector. */\nfunction encodeVector(embedding: number[]): Buffer {\n return Buffer.from(new Float32Array(embedding).buffer);\n}\n\n/**\n * `*` for no filter, else an AND of TAG clauses. A scalar value is an exact tag match\n * (`@meta_owner:{u1}`); an **array** value is a TAG alternation — match-any / OR\n * (`@meta_audience:{public|role\\:ADMIN}`), the capability-ACL primitive. RediSearch TAG fields are\n * multi-valued per document, so this also matches documents carrying several of the tags.\n */\nfunction buildFilter(filter?: Record<string, unknown>): string {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return '*';\n }\n const clauses = Object.entries(filter).map(([key, value]) => {\n const values = Array.isArray(value) ? value : [value];\n const alternation = values.map((entry) => escapeTag(String(entry))).join('|');\n return `@meta_${key}:{${alternation}}`;\n });\n return `(${clauses.join(' ')})`;\n}\n\n/**\n * Reduce a raw user query to RediSearch query *terms*, the union of which becomes the text clause.\n *\n * This is the escaping story for the text leg, and it escapes by **construction** rather than by\n * quoting: a term is kept only as a run of letters, digits and `_`, so nothing that could carry\n * meaning in RediSearch's query language — `@ { } | ( ) * ~ - \" ' : ; , => %` and friends — can\n * survive into the emitted query. A query cannot therefore add a clause, negate one, open a field\n * selector, or widen the filter it is ANDed with. (Escaping each punctuation character instead, as\n * {@link escapeTag} does for TAG values, would be safe but *lossy*: RediSearch's own tokenizer split\n * the indexed text on that same punctuation, so an escaped `hello\\-world` term matches nothing.)\n *\n * Unicode letters and digits are kept — stripping to ASCII would silently make non-English corpora\n * unsearchable. Since `\\p{L}`/`\\p{N}` contain no RediSearch metacharacters, keeping them is safe.\n */\nfunction tokenizeQuery(query: string): string[] {\n return query.match(/[\\p{L}\\p{N}_]+/gu) ?? [];\n}\n\n/**\n * Encode one metadata value as the `meta_<field>` hash field RediSearch indexes as a TAG. An **array**\n * becomes a multi-valued TAG (comma-separated, RediSearch's default separator) so a document can\n * carry several capability tokens — which is also why individual values must not contain commas.\n *\n * Shared by `upsert` and `updateMetadata` on purpose: they write the same field, and the day the two\n * encode it differently is the day a patched chunk stops matching a filter a freshly-ingested one\n * matches, for a value that looks identical in `metadata_json`.\n */\nfunction encodeTag(value: unknown): string {\n return Array.isArray(value) ? value.map(String).join(',') : String(value);\n}\n\n/** Escape RediSearch TAG punctuation so an id/tenant value matches literally. */\nfunction escapeTag(value: string): string {\n return value.replace(/[^a-zA-Z0-9_]/g, (char) => `\\\\${char}`);\n}\n\n/** Escape `SCAN MATCH` glob metacharacters so a document id matches its chunk keys literally. */\nfunction escapeGlob(value: string): string {\n return value.replace(/[*?[\\]\\\\]/g, (char) => `\\\\${char}`);\n}\n\n/**\n * Parse a `SCAN` reply into `{ cursor, keys }`, tolerating both wire shapes: the RESP2 array\n * `[cursor, [key, …]]` and node-redis's RESP3 object `{ cursor, keys }`.\n */\nfunction parseScanReply(reply: unknown): { cursor: string; keys: string[] } {\n if (Array.isArray(reply) && reply.length >= 2) {\n const rawKeys = reply[1];\n return {\n cursor: toStr(reply[0]),\n keys: Array.isArray(rawKeys) ? rawKeys.map(toStr) : [],\n };\n }\n if (typeof reply === 'object' && reply !== null && 'cursor' in reply && 'keys' in reply) {\n const { cursor, keys } = reply as { cursor: unknown; keys: unknown };\n return {\n cursor: toStr(cursor),\n keys: Array.isArray(keys) ? keys.map(toStr) : [],\n };\n }\n return { cursor: '0', keys: [] };\n}\n\nfunction toStr(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);\n}\n\n/**\n * Which leg produced the reply, and therefore where a passage's `score` comes from:\n *\n * - `vector` — the KNN query. Score is the returned `vector_score` **distance**, turned into a\n * cosine similarity by `1 - distance`. Two array elements per result (`key`, `fields`).\n * - `lexical` — the `WITHSCORES` text query. The engine's BM25 relevance is already a\n * bigger-is-better score, and it arrives *inline*: three array elements per result\n * (`key`, `score`, `fields`), which is why the stride is not a constant.\n */\ntype ScoreSource = 'vector' | 'lexical';\n\n/**\n * Parse an `FT.SEARCH` reply into passages, tolerating both wire shapes: the RESP2 array\n * `[total, key, [f, v, …], …]` (ioredis / node-redis RESP2) and the RESP3 object\n * `{ results: [{ id, extra_attributes: { … } }] }` (node-redis's default). The `prefix` is stripped\n * back off the key to recover the chunk id; `scoreSource` selects the scoring convention.\n */\nfunction parseSearchReply(\n reply: unknown,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n): Passage[] {\n const stride = scoreSource === 'lexical' ? 3 : 2;\n if (Array.isArray(reply)) {\n const passages: Passage[] = [];\n for (let index = 1; index + stride - 1 < reply.length; index += stride) {\n const rawFields = reply[index + stride - 1];\n if (Array.isArray(rawFields)) {\n const attrs: Record<string, string> = {};\n for (let field = 0; field + 1 < rawFields.length; field += 2) {\n attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);\n }\n const inlineScore = scoreSource === 'lexical' ? toStr(reply[index + 1]) : undefined;\n passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));\n }\n }\n return passages;\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n if (Array.isArray(results)) {\n return results.map((result) =>\n toPassage(\n readId(result),\n readAttributes(result),\n prefix,\n scoreSource,\n scoreSource === 'lexical' ? readScore(result) : undefined,\n ),\n );\n }\n }\n return [];\n}\n\n/** RESP3 `WITHSCORES` puts the relevance score on the result object rather than inline in the array. */\nfunction readScore(result: unknown): string | undefined {\n if (typeof result === 'object' && result !== null && 'score' in result) {\n return toStr((result as { score: unknown }).score);\n }\n return undefined;\n}\n\nfunction readId(result: unknown): string {\n if (typeof result === 'object' && result !== null && 'id' in result) {\n return toStr((result as { id: unknown }).id);\n }\n return '';\n}\n\nfunction readAttributes(result: unknown): Record<string, string> {\n const attrs: Record<string, string> = {};\n if (typeof result === 'object' && result !== null && 'extra_attributes' in result) {\n const raw = (result as { extra_attributes: unknown }).extra_attributes;\n if (typeof raw === 'object' && raw !== null) {\n for (const [key, value] of Object.entries(raw)) {\n attrs[key] = toStr(value);\n }\n }\n }\n return attrs;\n}\n\nfunction toPassage(\n rawId: string,\n attrs: Record<string, string>,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n inlineScore?: string,\n): Passage {\n const score =\n scoreSource === 'lexical' ? Number(inlineScore ?? '0') : 1 - Number(attrs.vector_score ?? '1');\n const source = attrs.source;\n return {\n id: rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId,\n text: attrs.text ?? '',\n score: Number.isFinite(score) ? score : 0,\n ...(source !== undefined ? { source } : {}),\n ...parseMetadata(attrs.metadata_json),\n };\n}\n\nfunction parseMetadata(raw: string | undefined): { metadata?: Record<string, unknown> } {\n if (raw === undefined) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed === 'object' && parsed !== null) {\n return { metadata: parsed as Record<string, unknown> };\n }\n } catch {\n // not JSON — no metadata\n }\n return {};\n}\n","/**\n * What a retrieval was served BY — the static half of retrieval telemetry, separated from the\n * emitting half (`retrieval-telemetry.ts`) so a store or a retriever can describe itself without\n * importing anything that emits. That separation is not tidiness: `retrieval-telemetry.ts` imports\n * the retrievers' shapes to detect them, and the retrievers import their descriptor kind, so putting\n * both halves in one module would make every retriever ↔ telemetry edge a cycle.\n */\n\n/** Which retrieval strategy answered. `unknown` is a retriever this package did not ship. */\nexport type RetrieverKind =\n | 'embedding'\n | 'keyword'\n | 'lexical'\n | 'hybrid'\n | 'filtered'\n | 'reranking'\n | 'unknown';\n\n/** Which backend held the chunks. Omitted entirely for a retriever that holds its own (keyword). */\nexport type VectorStoreKind = 'memory' | 'pg' | 'redis';\n\n/**\n * A retriever's (or a store's) self-description, as it appears on a retrieval event. Every field is\n * optional because the describer is answering only for the layer it *is*: a store knows its backend\n * and namespace but not which strategy queried it; `KeywordRetriever` knows its strategy but has no\n * store at all.\n */\nexport interface RetrievalDescriptor {\n retriever?: RetrieverKind;\n store?: VectorStoreKind;\n /**\n * The namespace the chunks live in — `PgVectorStore`'s table, `RedisVectorStore`'s RediSearch\n * index. This is the STORE-level namespace, not a per-query metadata scope: a host that partitions\n * one index into logical collections with a metadata filter (the usual multi-tenant shape) reports\n * that through `InstrumentRetrieverOptions.collection`, which overrides this.\n */\n collection?: string;\n}\n\n/**\n * The optional capability a store or retriever implements to appear on a retrieval event as\n * something other than `unknown`. Deliberately NOT a member of `VectorStore` or of core's\n * `Retriever`: both are implemented outside this package, and adding a required member to either\n * would break every existing implementation on upgrade — for a field that only observability reads.\n */\nexport interface DescribesRetrieval {\n describeRetrieval(): RetrievalDescriptor;\n}\n\n/** Structural guard, so a `VectorStore`/`Retriever` is narrowed to the capability without a cast. */\nexport function describesRetrieval(value: unknown): value is DescribesRetrieval {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof Reflect.get(value, 'describeRetrieval') === 'function'\n );\n}\n\n/** A candidate's descriptor, or an empty one when it does not carry the capability. */\nexport function describeRetrieval(candidate: unknown): RetrievalDescriptor {\n return describesRetrieval(candidate) ? candidate.describeRetrieval() : {};\n}\n\n/**\n * The store half of a wrapped retriever's descriptor: the base's `store`/`collection` with its\n * `retriever` dropped, so the wrapper can stamp its own strategy on top.\n *\n * `new FilteredRetriever(new EmbeddingRetriever(embedder, pg), scope)` therefore reports\n * `{ retriever: 'filtered', store: 'pg', collection: 'agent_rag_chunks' }` rather than losing the\n * store: which backend answered is the thing a \"retrievals by store\" panel exists to show, and a\n * production wiring is almost always two or three wrappers deep.\n */\nexport function describeSource(base: unknown): Omit<RetrievalDescriptor, 'retriever'> {\n const { retriever: _ignored, ...source } = describeRetrieval(base);\n return source;\n}\n\n/**\n * The store half shared by EVERY leg of a composed retriever, or nothing when they disagree.\n *\n * A hybrid whose legs are the dense and lexical halves of the same RediSearch index (the shape\n * `LexicalRetriever` exists for) genuinely was served by that one store, and reporting it is what\n * makes the store breakdown add up. A hybrid fusing pgvector with an in-process BM25 index was not\n * served by either alone, and picking the first leg would silently attribute every such retrieval to\n * a store that answered half of it.\n */\nexport function describeSharedSource(\n legs: readonly unknown[],\n): Omit<RetrievalDescriptor, 'retriever'> {\n const sources = legs.map(describeSource);\n const [first] = sources;\n if (first === undefined) {\n return {};\n }\n const agrees = sources.every(\n (source) => source.store === first.store && source.collection === first.collection,\n );\n return agrees ? first : {};\n}\n","import type {\n EmbeddingProvider,\n Passage,\n RetrieveOptions,\n Retriever,\n} from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\nimport type { VectorStore } from './vector-store.js';\n\nexport interface EmbeddingRetrieverOptions {\n /**\n * Drop passages scoring below this, so a query the corpus cannot answer returns nothing.\n *\n * A vector search always returns its `topK` nearest neighbours, and \"nearest\" is not \"relevant\".\n * Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least\n * unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result\n * says the answer is absent, so without a floor the consumer either feeds a model five irrelevant\n * passages or invents a threshold of its own.\n *\n * **This is the only place in the package where a threshold is meaningful, which is why it lives\n * here and not on `HybridRetriever`.** The score here comes straight from the store's vector\n * search: a calibrated similarity, comparable across queries and monotonic in how well the passage\n * matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each\n * leg, then fuse; never the other way round.\n *\n * Pick the number by measuring, not by taste: score questions you know the corpus answers against\n * questions you know it does not, and put the floor in the gap between the two populations. It is\n * specific to the embedding model and the distance metric, so it does not survive changing either.\n * Left unset nothing is dropped, which is the previous behaviour exactly.\n */\n minScore?: number;\n}\n\n/**\n * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:\n * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool\n * via `createRetrievalTool`, or into `AgentModule.forRoot({ retrieval })` for inject mode).\n */\nexport class EmbeddingRetriever implements Retriever {\n constructor(\n private readonly embedder: EmbeddingProvider,\n private readonly store: VectorStore,\n private readonly options: EmbeddingRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the dense strategy, over whatever store it was handed. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'embedding', ...describeSource(this.store) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const [embedding] = await this.embedder.embed([query]);\n if (embedding === undefined) {\n return [];\n }\n const passages = await this.store.search(embedding, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n const { minScore } = this.options;\n // Applied here rather than pushed into the store's query: not every backend can express \"score\n // above X\" in the same call as its KNN search, and filtering after keeps the floor byte-identical\n // across all three adapters instead of subtly engine-specific. It also keeps `topK` meaning \"ask\n // the store for this many\", so a floor never silently turns into a smaller search.\n return minScore === undefined\n ? passages\n : passages.filter((passage) => passage.score >= minScore);\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport type { ChunkRecord } from './ingest.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport { documentIdOf } from './vector-store.js';\n\nexport interface KeywordRetrieverOptions {\n /** BM25 term-frequency saturation. Default 1.5. */\n k1?: number;\n /** BM25 length normalization, 0 (off) … 1 (full). Default 0.75. */\n b?: number;\n}\n\ninterface IndexedDoc {\n record: ChunkRecord;\n /** term → frequency within this doc. */\n frequencies: Map<string, number>;\n length: number;\n}\n\n/**\n * An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME\n * {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids\n * line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation\n * (`k1`), and document-length normalization (`b`).\n *\n * Because it holds its OWN copy of each chunk's text, it needs the same delete-sync as the vector\n * store: whenever you call {@link import('./vector-store.js').VectorStore.remove}, call\n * {@link KeywordRetriever.remove} with the same document id. Skipping it leaves the document\n * lexically retrievable — `retrieve` would keep returning the removed passage's full text.\n */\nexport class KeywordRetriever implements Retriever {\n private readonly docs = new Map<string, IndexedDoc>();\n private readonly documentFrequency = new Map<string, number>();\n private totalLength = 0;\n private readonly k1: number;\n private readonly b: number;\n\n constructor(options: KeywordRetrieverOptions = {}) {\n this.k1 = options.k1 ?? 1.5;\n this.b = options.b ?? 0.75;\n }\n\n /**\n * Telemetry self-description. No `store`: this retriever holds its own copy of the corpus in\n * process, so attributing its retrievals to whichever vector store happens to hold the same chunks\n * would credit a backend that was never queried.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'keyword' };\n }\n\n /** How many chunks are currently indexed. */\n get size(): number {\n return this.docs.size;\n }\n\n /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */\n add(records: ChunkRecord[]): void {\n for (const record of records) {\n this.removeChunk(record.id);\n const tokens = tokenize(record.text);\n const frequencies = new Map<string, number>();\n for (const token of tokens) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + 1);\n }\n for (const term of frequencies.keys()) {\n this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1);\n }\n this.docs.set(record.id, { record, frequencies, length: tokens.length });\n this.totalLength += tokens.length;\n }\n }\n\n /**\n * Forget a source document — every chunk whose id is `${documentId}` or `${documentId}#<n>`, the\n * same id scheme (and the same {@link documentIdOf} collapse) the vector stores use for their\n * `remove`. The keyword half of a hybrid index keeps its own copy of the chunk text, so a document\n * dropped from the vector store stays fully retrievable here until this is called. Unknown ids are\n * a no-op.\n */\n remove(documentId: string): void {\n for (const id of [...this.docs.keys()]) {\n if (id === documentId || documentIdOf(id) === documentId) {\n this.removeChunk(id);\n }\n }\n }\n\n /** Drop every indexed chunk, resetting the corpus statistics with it. */\n clear(): void {\n this.docs.clear();\n this.documentFrequency.clear();\n this.totalLength = 0;\n }\n\n /** Drop ONE chunk by its exact id, decrementing the corpus statistics it contributed. */\n private removeChunk(id: string): void {\n const existing = this.docs.get(id);\n if (existing === undefined) {\n return;\n }\n for (const term of existing.frequencies.keys()) {\n const next = (this.documentFrequency.get(term) ?? 1) - 1;\n if (next <= 0) {\n this.documentFrequency.delete(term);\n } else {\n this.documentFrequency.set(term, next);\n }\n }\n this.totalLength -= existing.length;\n this.docs.delete(id);\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const total = this.docs.size;\n if (total === 0) {\n return [];\n }\n const averageLength = this.totalLength / total;\n const queryTerms = new Set(tokenize(query));\n const scored: Passage[] = [];\n for (const { record, frequencies, length } of this.docs.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n let score = 0;\n for (const term of queryTerms) {\n const termFrequency = frequencies.get(term);\n if (termFrequency === undefined) {\n continue;\n }\n const df = this.documentFrequency.get(term) ?? 0;\n const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5));\n const denominator =\n termFrequency + this.k1 * (1 - this.b + (this.b * length) / averageLength);\n score += idf * ((termFrequency * (this.k1 + 1)) / denominator);\n }\n if (score > 0) {\n scored.push({\n id: record.id,\n text: record.text,\n score,\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK ?? 5);\n }\n}\n\nfunction tokenize(text: string): string[] {\n return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\nimport type { LexicalVectorStore } from './vector-store.js';\n\n/**\n * The keyword half of hybrid search, served by the store's own full-text index — the mirror of\n * {@link import('./embedding-retriever.js').EmbeddingRetriever}, which serves the dense half. It\n * holds no state: every query goes to the store, so any process that can reach the store can run\n * lexical search, whether or not it did the ingestion.\n *\n * Compose it exactly like the dense leg:\n *\n * ```ts\n * const retriever = new HybridRetriever([\n * new EmbeddingRetriever(embedder, store),\n * new LexicalRetriever(store),\n * ]);\n * ```\n *\n * Both legs read the same chunks from the same index, so their ids line up by construction and\n * `HybridRetriever`'s RRF fuses them without either side needing a score on a common scale.\n *\n * Prefer this over {@link import('./keyword-retriever.js').KeywordRetriever} whenever the store\n * supports it (see {@link import('./vector-store.js').isLexicalVectorStore}) and ingestion and\n * search can run in different processes; `KeywordRetriever` remains the answer for `MemoryVectorStore`\n * and single-process deployments.\n */\nexport class LexicalRetriever implements Retriever {\n constructor(private readonly store: LexicalVectorStore) {}\n\n /** Telemetry self-description — the store's own lexical index, so the store is the same one. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'lexical', ...describeSource(this.store) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.store.searchText(query, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSharedSource } from './retrieval-descriptor.js';\n\nexport interface HybridRetrieverOptions {\n /** RRF constant — larger flattens the rank weighting. Default 60 (the value from the original paper). */\n k?: number;\n /** Candidates pulled from each retriever before fusion. Default 20. */\n fetchTopK?: number;\n /** Per-retriever multipliers on each RRF contribution (same order as `retrievers`). Default all 1. */\n weights?: number[];\n}\n\n/**\n * Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense\n * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the\n * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs\n * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.\n *\n * ## The score on a fused passage is not a similarity. Do not threshold it.\n *\n * Read this before using `passage.score` for anything other than the order it already produced.\n *\n * Not normalizing between scales is what makes RRF robust, and the price is that the number it\n * emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that\n * bite in practice:\n *\n * - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though\n * it arrives in the same field, of the same type, from the same `Retriever` interface. There is no\n * discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you\n * surface scores to a user or a model, know which retriever produced them.\n * - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in\n * `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,\n * `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the\n * least-bad member of a set of terrible matches both come back around `0.016`.\n *\n * The second point is why a relevance floor cannot live here. What the fused score actually measures\n * is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs\n * return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the\n * same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone\n * separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same\n * queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not\n * appear; what does not change is that both numbers live in a band roughly one part in fifty wide,\n * with no room for a threshold to mean anything.\n *\n * So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what\n * survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here\n * would be an invitation to apply it to a number that cannot support one.\n */\nexport class HybridRetriever implements Retriever {\n constructor(\n private readonly retrievers: Retriever[],\n private readonly options: HybridRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'hybrid', ...describeSharedSource(this.retrievers) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const k = this.options.k ?? 60;\n const fetchTopK = this.options.fetchTopK ?? 20;\n const weights = this.options.weights;\n const lists = await Promise.all(\n this.retrievers.map((retriever) =>\n retriever.retrieve(query, {\n topK: fetchTopK,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n }),\n ),\n );\n\n const fused = new Map<string, { passage: Passage; score: number }>();\n lists.forEach((list, listIndex) => {\n const weight = weights?.[listIndex] ?? 1;\n list.forEach((passage, rank) => {\n const contribution = weight / (k + rank + 1);\n const existing = fused.get(passage.id);\n if (existing !== undefined) {\n existing.score += contribution;\n } else {\n fused.set(passage.id, { passage, score: contribution });\n }\n });\n });\n\n return [...fused.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, options.topK ?? 5)\n .map((entry) => ({ ...entry.passage, score: entry.score }));\n }\n}\n","import type { Passage, Reranker, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\n\nexport interface RerankingRetrieverOptions {\n /** How many candidates to pull from the base retriever before reranking. Default 20. */\n fetchTopK?: number;\n}\n\n/**\n * Two-stage retrieval: over-fetch cheap candidates from a base `Retriever`, then reorder them with a\n * stronger {@link Reranker} and keep the top few. The standard precision boost — a fast first stage\n * casts a wide net, a slow accurate second stage sharpens it. Composes over ANY retriever (vector,\n * hybrid, keyword), and is itself a `Retriever`.\n */\nexport class RerankingRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly reranker: Reranker,\n private readonly options: RerankingRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the two-stage strategy, over the base's store. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'reranking', ...describeSource(this.base) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const candidates = await this.base.retrieve(query, {\n topK: this.options.fetchTopK ?? 20,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n return this.reranker.rerank(query, candidates, { topK: options.topK ?? 5 });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\n\n/**\n * Wraps a `Retriever` so every `retrieve` call ANDs a fixed metadata `filter` on top of whatever the\n * caller passes — the owner/tenant-scoping primitive. `new FilteredRetriever(base, { ownerId })` only\n * ever returns that owner's passages, so a shared vector store serves per-user / per-tenant RAG\n * without the agent (or a crafted query) reaching across the boundary.\n *\n * The fixed filter takes precedence on key conflicts: a caller can narrow further but cannot widen\n * past the scope. Composes over ANY retriever (vector, hybrid, reranked) and is itself a `Retriever`.\n */\nexport class FilteredRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly filter: Record<string, unknown>,\n ) {}\n\n /** Telemetry self-description — this scope, over the base's store. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'filtered', ...describeSource(this.base) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.base.retrieve(query, {\n ...options,\n filter: { ...options.filter, ...this.filter },\n });\n }\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\nimport { type ChunkOptions, chunkText } from './chunk.js';\nimport type { VectorRecord, VectorStore } from './vector-store.js';\n\n/** A source document to ingest. `id` scopes the chunk ids (`${id}#${n}`); `source` is the citation. */\nexport interface IngestDocument {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\n/** A chunked, not-yet-embedded record. Feed the same array to a vector store AND a keyword index so\n * both retrievers key on identical chunk ids (which is what lets hybrid fusion line them up). */\nexport interface ChunkRecord {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface IngestChunksOptions {\n embedder: EmbeddingProvider;\n store: VectorStore;\n}\n\nexport interface IngestOptions extends ChunkOptions, IngestChunksOptions {}\n\n/**\n * Split documents into chunk records. Chunk ids are `${doc.id}#${index}`, so re-chunking the same\n * document produces the same ids (upsert overwrites in place rather than duplicating).\n */\nexport function chunkDocuments(\n documents: IngestDocument[],\n options: ChunkOptions = {},\n): ChunkRecord[] {\n const chunks: ChunkRecord[] = [];\n for (const document of documents) {\n chunkText(document.text, options).forEach((text, index) => {\n chunks.push({\n id: `${document.id}#${index}`,\n text,\n ...(document.source !== undefined ? { source: document.source } : {}),\n ...(document.metadata !== undefined ? { metadata: document.metadata } : {}),\n });\n });\n }\n return chunks;\n}\n\n/** Embed pre-chunked records (one batched `embed`) and upsert them. Returns the record count. */\nexport async function ingestChunks(\n chunks: ChunkRecord[],\n options: IngestChunksOptions,\n): Promise<number> {\n if (chunks.length === 0) {\n return 0;\n }\n const embeddings = await options.embedder.embed(chunks.map((chunk) => chunk.text));\n const records: VectorRecord[] = chunks.map((chunk, index) => ({\n ...chunk,\n embedding: embeddings[index] ?? [],\n }));\n await options.store.upsert(records);\n return records.length;\n}\n\n/** Chunk → embed → upsert in one call. For hybrid search, use {@link chunkDocuments} +\n * {@link ingestChunks} and feed the same chunks to a {@link import('./keyword-retriever.js').KeywordRetriever}. */\nexport async function ingestDocuments(\n documents: IngestDocument[],\n options: IngestOptions,\n): Promise<number> {\n return ingestChunks(chunkDocuments(documents, options), options);\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { channelName, emit, getChannel } from '@dudousxd/nestjs-diagnostics';\nimport {\n type RetrievalDescriptor,\n type RetrieverKind,\n type VectorStoreKind,\n describeRetrieval,\n} from './retrieval-descriptor.js';\n\n/**\n * Retrieval telemetry: one diagnostics event per retrieval, carrying how long it took, how much came\n * back, how good it was, and what served it.\n *\n * Until this existed, retrieval reached Telescope only as the *tool call* that wrapped it (see\n * `retrieval-tool.ts`), which says a retrieval happened and nothing else. The questions an operator\n * actually arrives with — is retrieval slow, is it returning nothing, is one collection colder than\n * the rest, did the scores collapse after a model swap — are all invisible in a tool-call row,\n * because a tool call that returns zero passages and one that returns five perfect ones are the same\n * row with the same `ok` status.\n *\n * ## One seam, not six\n *\n * Instrumentation is a WRAPPER over the core `Retriever` SPI ({@link instrumentRetriever}), not code\n * inside each retriever. Every retriever in this package — and every retriever outside it — is the\n * same one-method interface, so one wrapper covers all of them, including ones this package has\n * never heard of. The alternative, an `emit` inside each of the six `retrieve` methods, would have\n * had to be repeated in each, skipped by third-party retrievers entirely, and would have double- and\n * triple-counted the composed ones (a hybrid over two legs emitting three events for one retrieval).\n *\n * ## How a composed retrieval reports\n *\n * **The outermost instrumented retriever in a call tree emits; anything instrumented inside it stays\n * silent.** A retrieval is one question the caller asked, so it is one event no matter how many\n * retrievers cooperated to answer it: `RerankingRetriever` over a `HybridRetriever` over a dense and\n * a lexical leg is ONE retrieval, reported as `retriever: 'reranking'` with the duration of the\n * whole thing and the scores the caller actually got back.\n *\n * The suppression is enforced with an {@link AsyncLocalStorage} flag rather than by asking hosts to\n * wrap carefully, because the wrapping that double-counts is the natural one: a host that wraps its\n * top-level retriever and *also* wraps the dense leg it shares with another code path has not made a\n * mistake, and would otherwise get two events per retrieval, a zero-hit rate computed over a mixture\n * of legs and composites, and a retrieval count roughly twice its query count.\n *\n * ## Opt-out safety, and what the hot path pays\n *\n * The event goes out on `aviary:rag:retrieval` via `@dudousxd/nestjs-diagnostics`, whose `emit` is\n * gated on `channel.hasSubscribers` and never throws. This wrapper checks that same gate BEFORE it\n * does anything at all — no clock read, no `AsyncLocalStorage.run`, no score arithmetic — and\n * delegates straight through when nothing is listening. A host that wires this package without\n * Telescope (or any subscriber) pays two `Map` lookups and a boolean per retrieval, and nothing else\n * changes: the wrapper returns the base retriever's passages verbatim, and rethrows its errors\n * unchanged.\n *\n * ## Why this channel is not merged into the typed `ChannelRegistry`\n *\n * `@dudousxd/nestjs-agent-rag-media` already emits `media.ingested`/`media.removed`/`media.skipped`/\n * `media.failed` on the same `rag` lib, and flip's host app emits `rag:search` on it. Declaring\n * `ChannelRegistry['rag'] = { retrieval: RagRetrievalEvent }` here would narrow `EventOf<'rag'>` to\n * this one key process-wide and turn every one of those sibling emits into a compile error in any\n * project that installs both packages. The payload stays typed where it matters — {@link emitRagRetrieval}\n * takes {@link RagRetrievalEvent} — and the wire contract is documented below instead.\n */\n\n/** The diagnostics lib segment. Shared with `@dudousxd/nestjs-agent-rag-media`'s `media.*` events. */\nexport const RAG_DIAGNOSTIC_LIB = 'rag';\n\n/** The diagnostics event segment. */\nexport const RAG_RETRIEVAL_EVENT = 'retrieval';\n\n/**\n * The full channel name a subscriber listens on: `aviary:rag:retrieval`. This is the wire contract —\n * `@dudousxd/nestjs-agent-telescope` subscribes to this exact string rather than importing this\n * package, which is what keeps the telescope extension free of a dependency on the retrieval stack\n * (and lets a host that uses neither still observe retrieval with a bare `diagnostics_channel`\n * subscription).\n */\nexport const RAG_RETRIEVAL_CHANNEL = channelName(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT);\n\n/**\n * The payload of one retrieval event. Counts, durations, kinds and scores only — never the query\n * text and never passage text. Both are user content, and this lands in an ops store with a short\n * retention window and a Slack-facing redaction budget; everything here is safe to put on a\n * dashboard and is what tells the failure modes apart anyway.\n *\n * The DURATION is not in here: it rides the envelope (`emit`'s `opts.durationMs`), which is the field\n * Telescope's OTel exporter turns into a latency histogram. Putting it in the payload instead would\n * have made retrieval latency a number somebody reads out of a JSON blob rather than a graph.\n */\nexport interface RagRetrievalEvent {\n /** Which strategy answered — the OUTERMOST one, for a composed retriever. */\n retriever: RetrieverKind;\n /** Which backend held the chunks. Absent for an in-process retriever, or when legs disagree. */\n store?: VectorStoreKind;\n /** Store namespace (pg table / RediSearch index), or the host's logical collection. */\n collection?: string;\n /** What the caller asked for, so `chunks < topK` is readable as \"the corpus ran out\". */\n topK: number;\n /** How many passages came back. */\n chunks: number;\n /**\n * `chunks === 0`. Carried as its own flag rather than left to be derived, because the zero-hit RATE\n * is the headline number and deriving it means every consumer re-deriving the same predicate.\n */\n zeroHit: boolean;\n /** Best score in the result, rounded to 4dp. Absent on a zero-hit. */\n topScore?: number;\n /** Mean score across the result, rounded to 4dp. Absent on a zero-hit. */\n meanScore?: number;\n /**\n * The retrieval threw. Reported rather than dropped because a failed retrieval and an empty one\n * look identical from the agent's side — both become \"I couldn't find anything\" — so a channel\n * that only carried successes would go quiet exactly when retrieval broke.\n */\n failed: boolean;\n /** The error message, when `failed`. */\n error?: string;\n}\n\n/** Publish one retrieval event. `durationMs` rides the envelope; see {@link RagRetrievalEvent}. */\nexport function emitRagRetrieval(event: RagRetrievalEvent, durationMs: number): void {\n emit(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT, event, { durationMs });\n}\n\nexport interface InstrumentRetrieverOptions {\n /**\n * Override the auto-detected {@link RetrievalDescriptor}, field by field. Needed for a retriever or\n * store from outside this package (which reports `retriever: 'unknown'` and no store), and for a\n * host that wants its own naming.\n */\n describe?: RetrievalDescriptor;\n /**\n * The logical collection this retrieval searched, when the store namespace is not it. Pass a\n * function to read it off the call's metadata filter — the multi-tenant shape, where one index\n * holds every collection and the filter picks one:\n *\n * ```ts\n * instrumentRetriever(retriever, {\n * collection: (filter) => (typeof filter?.collectionId === 'string' ? filter.collectionId : undefined),\n * })\n * ```\n *\n * Returning `undefined` falls back to the store namespace, which is the right answer for a query\n * scoped to many collections at once: a breakdown panel needs ONE label per retrieval, and\n * inventing a synthetic `\"a+b+c\"` bucket would produce a chart whose slices are combinations\n * rather than collections.\n */\n collection?: string | ((filter: Record<string, unknown> | undefined) => string | undefined);\n}\n\n/**\n * The default a retriever falls back to when the caller names no `topK` — every retriever in this\n * package uses `options.topK ?? 5`, so reporting anything else here would make `chunks < topK`\n * misread as truncation on exactly the calls that did not ask for a limit.\n */\nconst DEFAULT_TOP_K = 5;\n\n/**\n * Set for the duration of an instrumented `retrieve`, so an instrumented retriever running INSIDE\n * another one knows it is a leg and stays silent. See the composed-retrieval section above.\n */\nconst inRetrieval = new AsyncLocalStorage<true>();\n\n/** Round to 4dp — enough to see a score distribution move, short enough to keep the payload small. */\nfunction round4(value: number): number {\n return Math.round(value * 10_000) / 10_000;\n}\n\n/** Top + mean score, skipping any passage whose `score` is not a finite number. */\nfunction scoreStats(passages: Passage[]): { topScore?: number; meanScore?: number } {\n const scores = passages.map((passage) => passage.score).filter((score) => Number.isFinite(score));\n if (scores.length === 0) {\n return {};\n }\n const sum = scores.reduce((total, score) => total + score, 0);\n return { topScore: round4(Math.max(...scores)), meanScore: round4(sum / scores.length) };\n}\n\nclass InstrumentedRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly options: InstrumentRetrieverOptions = {},\n ) {}\n\n /** Transparent to detection: wrapping a retriever must not turn it into an `unknown` one. */\n describeRetrieval(): RetrievalDescriptor {\n return { ...describeRetrieval(this.base), ...this.options.describe };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n // Both gates before any work: nothing subscribed, or we are a leg of an outer instrumented\n // retrieval that will report the whole thing itself.\n if (!getChannel(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT).hasSubscribers) {\n return this.base.retrieve(query, options);\n }\n if (inRetrieval.getStore() === true) {\n return this.base.retrieve(query, options);\n }\n\n const descriptor = this.describeRetrieval();\n const topK = options.topK ?? DEFAULT_TOP_K;\n const startedAt = Date.now();\n try {\n const passages = await inRetrieval.run(true, () => this.base.retrieve(query, options));\n emitRagRetrieval(\n {\n retriever: descriptor.retriever ?? 'unknown',\n ...(descriptor.store !== undefined ? { store: descriptor.store } : {}),\n ...this.collectionOf(options.filter, descriptor),\n topK,\n chunks: passages.length,\n zeroHit: passages.length === 0,\n ...scoreStats(passages),\n failed: false,\n },\n Date.now() - startedAt,\n );\n return passages;\n } catch (error) {\n emitRagRetrieval(\n {\n retriever: descriptor.retriever ?? 'unknown',\n ...(descriptor.store !== undefined ? { store: descriptor.store } : {}),\n ...this.collectionOf(options.filter, descriptor),\n topK,\n chunks: 0,\n zeroHit: true,\n failed: true,\n error: error instanceof Error ? error.message : String(error),\n },\n Date.now() - startedAt,\n );\n throw error;\n }\n }\n\n /** The host's collection for this call, else the store namespace, else nothing. */\n private collectionOf(\n filter: Record<string, unknown> | undefined,\n descriptor: RetrievalDescriptor,\n ): { collection?: string } {\n const { collection } = this.options;\n const resolved =\n typeof collection === 'function' ? collection(filter) : (collection ?? descriptor.collection);\n return resolved !== undefined ? { collection: resolved } : {};\n }\n}\n\n/**\n * Wrap a retriever so every `retrieve` publishes a {@link RagRetrievalEvent} on\n * {@link RAG_RETRIEVAL_CHANNEL}. Wrap the retriever you actually hand to the agent — the outermost\n * one — and nothing else needs changing:\n *\n * ```ts\n * const retriever = instrumentRetriever(\n * new HybridRetriever([new EmbeddingRetriever(embedder, store), new LexicalRetriever(store)]),\n * );\n * ```\n *\n * {@link createRetrievalTool} already does this for the tool it builds, so agentic retrieval is\n * instrumented with no host change at all; call this directly for inject-mode retrieval\n * (`AgentModule.forRoot({ retrieval })`) or for a host that calls `retrieve` itself. Wrapping twice\n * is harmless — see the composed-retrieval section above.\n */\nexport function instrumentRetriever(\n retriever: Retriever,\n options: InstrumentRetrieverOptions = {},\n): Retriever {\n return new InstrumentedRetriever(retriever, options);\n}\n","import type { Passage, Retriever, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { type InstrumentRetrieverOptions, instrumentRetriever } from './retrieval-telemetry.js';\n\nexport interface RetrievalToolOptions {\n /** Tool name the model sees. Default `search_knowledge`. */\n name?: string;\n description?: string;\n /** Passages per search. Default lets the retriever decide (typically 5). */\n topK?: number;\n /**\n * Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store\n * and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic\n * mode: instrumenting here is what makes the RAG panels work for a host that only ever calls\n * `createRetrievalTool`, with no wiring at all. It costs nothing when nobody subscribes (see\n * {@link instrumentRetriever}), and cannot double-count when the host has already wrapped the\n * retriever itself.\n *\n * Pass `false` to opt a single tool out — a host that reports retrieval its own way, or one whose\n * corpus is small enough that the events are noise.\n */\n telemetry?: boolean | InstrumentRetrieverOptions;\n}\n\n/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */\nexport interface RetrievalTool {\n spec: ToolSpec;\n handler: ToolHandler;\n}\n\nfunction isQueryInput(value: unknown): value is { query: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'query' in value &&\n typeof (value as Record<'query', unknown>).query === 'string'\n );\n}\n\n/** A hand-rolled Standard Schema for `{ query: string }` — keeps this package validation-lib-free. */\nconst querySchema: StandardSchemaV1<{ query: string }> = {\n '~standard': {\n version: 1,\n vendor: 'nestjs-agent-rag',\n validate(value: unknown) {\n return isQueryInput(value)\n ? { value: { query: value.query } }\n : { issues: [{ message: 'retrieval tool expects { query: string }' }] };\n },\n },\n};\n\n/**\n * Builds the agentic-retrieval tool: a `read`-kind `search_knowledge(query)` the model calls when it\n * decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources\n * flow to the frontend and telescope through the normal tool-call machinery (the citation surface).\n *\n * The retrieval ITSELF is also instrumented (`aviary:rag:retrieval`) unless `telemetry: false` — the\n * tool-call row says a retrieval happened, the retrieval event says how long it took, how much came\n * back and how good it was. See {@link instrumentRetriever}.\n *\n * ```ts\n * providers: [provideAgentTool(createRetrievalTool(retriever))]\n * ```\n */\nexport function createRetrievalTool(\n retriever: Retriever,\n options: RetrievalToolOptions = {},\n): RetrievalTool {\n const topK = options.topK;\n const { telemetry = true } = options;\n const instrumented =\n telemetry === false\n ? retriever\n : instrumentRetriever(retriever, telemetry === true ? {} : telemetry);\n return {\n spec: {\n name: options.name ?? 'search_knowledge',\n kind: 'read',\n description:\n options.description ??\n 'Search the knowledge base for passages relevant to a query. Returns passages with their source for citation.',\n inputSchema: querySchema,\n },\n handler: {\n async execute(input: unknown): Promise<{ passages: Passage[] }> {\n if (!isQueryInput(input)) {\n throw new Error('retrieval tool expects { query: string }');\n }\n const passages = await instrumented.retrieve(\n input.query,\n topK !== undefined ? { topK } : {},\n );\n return { passages };\n },\n },\n };\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\n\n/** A non-2xx answer (or a malformed one) from an embeddings or rerank endpoint. */\nexport class HttpModelError extends Error {\n constructor(\n /** HTTP status; `502` when the endpoint answered 2xx with a body that doesn't fit the contract. */\n readonly status: number,\n message: string,\n ) {\n super(message);\n this.name = 'HttpModelError';\n }\n}\n\nexport interface OpenAiEmbeddingsOptions {\n /** The embedding model id, sent as `model`. */\n model: string;\n /**\n * Base URL up to and including `/v1` — the client posts to `<baseUrl>/embeddings`. Default\n * `https://api.openai.com/v1`. Point it at any OpenAI-compatible server: Azure/OpenRouter/an LLM\n * gateway, Ollama (`http://localhost:11434/v1`), Hugging Face TEI, vLLM, LocalAI, LM Studio…\n */\n baseUrl?: string;\n /** Sent as `Authorization: Bearer <apiKey>` when set. Local servers usually need none. */\n apiKey?: string;\n /** Output width for models that support shortening (`text-embedding-3-*`), sent as `dimensions`. */\n dimensions?: number;\n /** Inputs per request. Default 64. */\n batchSize?: number;\n /**\n * Inputs longer than this many characters are truncated before sending, so one oversized chunk\n * doesn't fail a whole batch on the model's context limit. Default 24 000 (≈ 6–8k tokens); set\n * `Infinity` to send inputs as they are.\n */\n maxInputChars?: number;\n /** Extra request headers (an org/project id, a gateway's attribution header). */\n headers?: Record<string, string>;\n /** Per-request timeout. Default 120 000 ms. */\n timeoutMs?: number;\n /** Called with the provider-reported token count of every request — for cost accounting. */\n onUsage?: (tokens: number) => void;\n /** `fetch` to use. Default the global one. */\n fetch?: typeof fetch;\n}\n\n/**\n * An {@link EmbeddingProvider} over any OpenAI-compatible `POST /v1/embeddings` endpoint, with no SDK\n * dependency: OpenAI itself, or a gateway/local server speaking the same protocol (TEI, Ollama,\n * vLLM, LocalAI…). For a Vercel AI SDK model use `aiSdkEmbedding` from `-ai-sdk` instead.\n *\n * Batches by {@link OpenAiEmbeddingsOptions.batchSize}, restores input order from each item's\n * `index`, and fails loudly (an {@link HttpModelError}) on a non-2xx answer or a vector count that\n * doesn't match the inputs. Empty strings are sent as a single space, since OpenAI rejects them.\n *\n * ```ts\n * const embedder = openAiEmbeddings({ model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY });\n * const local = openAiEmbeddings({ baseUrl: 'http://tei:8080/v1', model: 'BAAI/bge-m3' });\n * ```\n */\nexport function openAiEmbeddings(options: OpenAiEmbeddingsOptions): EmbeddingProvider {\n const url = `${(options.baseUrl ?? 'https://api.openai.com/v1').replace(/\\/+$/, '')}/embeddings`;\n const batchSize = Math.max(1, Math.floor(options.batchSize ?? 64));\n const maxInputChars = options.maxInputChars ?? 24_000;\n const timeoutMs = options.timeoutMs ?? 120_000;\n const doFetch = options.fetch ?? fetch;\n\n return {\n async embed(texts: string[]): Promise<number[][]> {\n const vectors: number[][] = [];\n for (let start = 0; start < texts.length; start += batchSize) {\n const input = texts\n .slice(start, start + batchSize)\n .map(\n (text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || ' ',\n );\n const response = await doFetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n input,\n ...(options.dimensions !== undefined ? { dimensions: options.dimensions } : {}),\n }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n const body = await response.text();\n if (!response.ok) {\n throw new HttpModelError(\n response.status,\n `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`,\n );\n }\n const json = parseJson(body) as {\n data?: { embedding?: unknown; index?: number }[];\n usage?: { prompt_tokens?: number; total_tokens?: number };\n } | null;\n const items = [...(json?.data ?? [])].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));\n if (\n items.length !== input.length ||\n !items.every((item) => Array.isArray(item.embedding))\n ) {\n throw new HttpModelError(\n 502,\n `Embeddings returned ${items.length} vectors for ${input.length} inputs`,\n );\n }\n for (const item of items) {\n vectors.push(item.embedding as number[]);\n }\n const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;\n if (typeof tokens === 'number' && Number.isFinite(tokens)) {\n options.onUsage?.(tokens);\n }\n }\n return vectors;\n },\n };\n}\n\nfunction parseJson(body: string): unknown {\n try {\n return JSON.parse(body);\n } catch {\n return null;\n }\n}\n\n/** The human part of an error body: OpenAI's `error.message`, a bare `message`/`error`, or the text. */\nexport function errorMessageOf(body: string): string {\n const json = parseJson(body) as {\n error?: { message?: unknown } | string;\n message?: unknown;\n } | null;\n const message =\n typeof json?.error === 'string'\n ? json.error\n : (json?.error?.message ?? json?.message ?? undefined);\n return typeof message === 'string' ? message : body.slice(0, 300);\n}\n","import type { Passage, RerankOptions, Reranker } from '@dudousxd/nestjs-agent-core';\nimport { HttpModelError, errorMessageOf } from './openai-embeddings.js';\n\nexport interface HttpRerankerOptions {\n /**\n * The full rerank endpoint: `https://api.cohere.com/v2/rerank`, `https://api.jina.ai/v1/rerank`,\n * `https://api.voyageai.com/v1/rerank`, or a Hugging Face TEI / Infinity / vLLM server's `/rerank`.\n */\n url: string;\n /** Model id, sent as `model` (hosted APIs require it; TEI serves one model and ignores it). */\n model?: string;\n /** Sent as `Authorization: Bearer <apiKey>` when set. */\n apiKey?: string;\n /**\n * Request body dialect. `cohere` (Cohere, Jina, Voyage, Infinity, vLLM): `{ query, documents,\n * top_n }`. `tei`: `{ query, texts }`. Default `auto`, which sends `documents` **and** `texts` so\n * one config works against both families. Responses are read in every dialect regardless.\n */\n format?: 'auto' | 'cohere' | 'tei';\n /** Extra request headers. */\n headers?: Record<string, string>;\n /** Per-request timeout. Default 30 000 ms. */\n timeoutMs?: number;\n /** `fetch` to use. Default the global one. */\n fetch?: typeof fetch;\n}\n\ninterface RerankItem {\n index?: number;\n relevance_score?: number;\n score?: number;\n}\n\n/**\n * A cross-encoder {@link Reranker} behind an HTTP `/rerank` endpoint — the Cohere-style contract\n * most providers and self-hosted servers share. Compose it with `RerankingRetriever`:\n *\n * ```ts\n * const reranker = new HttpReranker({ url: 'https://api.cohere.com/v2/rerank', model: 'rerank-v3.5', apiKey });\n * const retriever = new RerankingRetriever(hybrid, reranker, { fetchTopK: 30 });\n * ```\n *\n * Reads `results: [{ index, relevance_score }]` (Cohere, Jina), `data: [{ index, relevance_score }]`\n * (Voyage) and a bare `[{ index, score }]` (TEI). Each passage keeps its fields and takes the new\n * score; the list is re-sorted and cut to `topK`. Indices the server invents are ignored.\n */\nexport class HttpReranker implements Reranker {\n private readonly format: NonNullable<HttpRerankerOptions['format']>;\n private readonly doFetch: typeof fetch;\n\n constructor(private readonly options: HttpRerankerOptions) {\n this.format = options.format ?? 'auto';\n this.doFetch = options.fetch ?? fetch;\n }\n\n async rerank(\n query: string,\n passages: Passage[],\n options: RerankOptions = {},\n ): Promise<Passage[]> {\n if (passages.length === 0) {\n return [];\n }\n const topK = options.topK ?? passages.length;\n const documents = passages.map((passage) => passage.text);\n const response = await this.doFetch(this.options.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(this.options.apiKey ? { authorization: `Bearer ${this.options.apiKey}` } : {}),\n ...this.options.headers,\n },\n body: JSON.stringify({\n ...(this.options.model !== undefined ? { model: this.options.model } : {}),\n query,\n ...(this.format !== 'tei' ? { documents, top_n: topK } : {}),\n ...(this.format !== 'cohere' ? { texts: documents } : {}),\n }),\n signal: AbortSignal.timeout(this.options.timeoutMs ?? 30_000),\n });\n const body = await response.text();\n if (!response.ok) {\n throw new HttpModelError(\n response.status,\n `Rerank request failed (${response.status}): ${errorMessageOf(body)}`,\n );\n }\n const items = itemsOf(body);\n if (items === undefined) {\n throw new HttpModelError(502, 'Rerank response has no results');\n }\n const seen = new Set<number>();\n const reranked: Passage[] = [];\n for (const item of items) {\n const index = item.index;\n const passage = typeof index === 'number' ? passages[index] : undefined;\n if (passage === undefined || seen.has(index as number)) {\n continue;\n }\n seen.add(index as number);\n reranked.push({ ...passage, score: Number(item.relevance_score ?? item.score ?? 0) });\n }\n reranked.sort((a, b) => b.score - a.score);\n return reranked.slice(0, topK);\n }\n}\n\nfunction itemsOf(body: string): RerankItem[] | undefined {\n let json: unknown;\n try {\n json = JSON.parse(body);\n } catch {\n return undefined;\n }\n if (Array.isArray(json)) {\n return json as RerankItem[];\n }\n const wrapped = json as { results?: unknown; data?: unknown } | null;\n const items = wrapped?.results ?? wrapped?.data;\n return Array.isArray(items) ? (items as RerankItem[]) : undefined;\n}\n"],"mappings":"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","names":["chunkText","text","options","chunkSize","overlap","normalized","trim","length","separator","undefined","chunkByRecord","chunks","start","end","Math","min","breakAt","window","slice","boundary","max","lastIndexOf","chunk","push","current","flush","record","split","trimmed","candidate","isLexicalVectorStore","store","searchText","UnsafeRemovalError","Error","reason","message","name","chunkIndexOf","chunkId","match","exec","undefined","Number","documentIdOf","replace","splitMetadataPatch","patch","set","remove","key","value","Object","entries","push","undefined","isEmptyMetadataPatch","keys","length","applyMetadataPatch","metadata","next","matchesFilter","metadata","filter","entries","Object","undefined","length","every","key","expected","Array","isArray","actual","some","value","includes","filterMatchesNothing","values","MemoryVectorStore","records","Map","describeRetrieval","store","upsert","record","set","id","remove","documentId","keys","documentIdOf","delete","updateMetadata","patch","isEmptyMetadataPatch","updated","metadata","applyMetadataPatch","listDocuments","filter","documents","values","undefined","matchesFilter","has","listChunks","options","chunks","push","index","chunkIndexOf","text","sort","a","b","offset","limit","slice","listDocumentIds","filterMatchesNothing","Set","add","removeMany","documentIds","length","wanted","removeWhere","Object","UnsafeRemovalError","removed","countChunks","count","search","embedding","scored","score","cosineSimilarity","source","topK","dot","normA","normB","Math","min","valueA","valueB","denominator","sqrt","words","s","Set","split","filter","Boolean","DEFAULT_STOP_WORDS","english","portuguese","spanish","TOKEN","keywordTerms","query","max","stopWords","tokens","toLowerCase","normalize","match","counts","Object","entries","map","stop","t","has","length","best","Math","n","slice","terms","some","hasSearchSyntax","test","anyTermTsquery","replace","join","ADAPTED","Symbol","for","toPgClient","source","isDrizzle","$client","unsafe","fromPostgresJs","fromQueryable","client","undefined","mark","Object","defineProperty","value","rowsOf","result","Array","isArray","rows","sql","query","text","params","begin","bind","transaction","work","Promise","resolve","tx","isNodePgPool","connection","connect","broken","runInTransaction","release","Error","isNodePgClient","onRollbackFailed","error","catch","totalCount","processID","connectionParameters","NUL_BYTE","String","fromCharCode","stripNulBytes","value","includes","split","join","Array","isArray","map","item","proto","Object","getPrototypeOf","prototype","fromEntries","entries","key","entry","PgVectorStore","client","table","dimensions","indexedDimensions","nullableEmbeddings","iterativeScan","efSearch","upsertBatchSize","iterativeScanSupport","options","toPgClient","width","Number","isInteger","Error","undefined","Set","Math","max","floor","describeRetrieval","store","collection","schemaStatements","mixed","column","statements","push","ensureSchema","statement","query","upsert","records","byId","Map","record","id","delete","set","unique","values","start","length","batch","slice","params","rows","text","source","metadata","base","JSON","stringify","embedding","toVectorLiteral","remove","documentId","DOCUMENT_ID_FROM_CHUNK","updateMetadata","patch","isEmptyMetadataPatch","splitMetadataPatch","listDocuments","filter","where","row","doc_id","listChunks","sql","CHUNK_INDEX_FROM_ID","limit","offset","index","chunk_index","listDocumentIds","filterMatchesNothing","removeMany","documentIds","removeWhere","keys","UnsafeRemovalError","countChunks","count","search","topK","conditions","whereConditions","vectorType","withSearchSettings","toPassage","buildWhere","read","transaction","bind","iterative","supportsIterativeScan","tx","then","major","minor","extversion","catch","PgLexicalVectorStore","tsConfig","tsColumn","anyTermFallback","stopWords","config","fullText","SQL_IDENTIFIER","test","DEFAULT_STOP_WORDS","tsvector","searchText","trim","hasSearchSyntax","strict","runTextSearch","terms","keywordTerms","runRankedTermSearch","tsquery","any","anyTermTsquery","score","stripped","clauses","scalar","keyParam","arrParam","RedisVectorSchemaMismatchError","Error","index","field","expected","actual","remedy","name","RedisVectorStore","prefix","dimensions","filterableFields","lexicalScorer","client","options","describeRetrieval","store","collection","ensureSchema","info","sendCommand","createIndex","reconcileSchema","attributes","parseIndexAttributes","size","vector","get","undefined","missing","existing","push","type","schema","String","upsert","records","record","args","id","text","encodeVector","embedding","source","metadata","JSON","stringify","value","encodeTag","remove","documentId","keys","chunkKeys","length","updateMetadata","patch","isEmptyMetadataPatch","updated","key","raw","current","parseMetadata","toStr","next","applyMetadataPatch","stale","Set","bare","exists","Number","add","pattern","escapeGlob","cursor","reply","page","parseScanReply","listChunks","ordered","map","slice","entry","chunkIndexOf","sort","a","b","offset","limit","chunks","fields","Array","isArray","listDocuments","filter","filterMatchesNothing","query","buildFilter","documents","Map","batchSize","passages","parseSearchReply","passage","documentIdOf","has","set","values","listDocumentIds","SCAN_PAGE","parseSearchKeys","stripPrefix","removeMany","documentIds","wanted","pending","DELETE_BATCH","removeWhere","Object","UnsafeRemovalError","unindexed","includes","named","join","declared","removed","deleted","isFinite","countChunks","parseSearchTotal","search","topK","searchText","terms","tokenizeQuery","filterQuery","textQuery","startsWith","results","readId","total","total_results","readInfoField","readKeyValues","attribute","identifier","dim","toUpperCase","toLowerCase","entries","Buffer","from","Float32Array","buffer","clauses","alternation","escapeTag","match","replace","char","rawKeys","isBuffer","toString","scoreSource","stride","rawFields","attrs","inlineScore","toPassage","result","readAttributes","readScore","score","extra_attributes","rawId","vector_score","metadata_json","parsed","parse","describesRetrieval","value","Reflect","get","describeRetrieval","candidate","describeSource","base","retriever","_ignored","source","describeSharedSource","legs","sources","map","first","undefined","agrees","every","store","collection","EmbeddingRetriever","embedder","store","options","describeRetrieval","retriever","describeSource","retrieve","query","embedding","embed","undefined","passages","search","topK","filter","minScore","passage","score","KeywordRetriever","docs","Map","documentFrequency","totalLength","k1","b","options","describeRetrieval","retriever","size","add","records","record","removeChunk","id","tokens","tokenize","text","frequencies","token","set","get","term","keys","length","remove","documentId","documentIdOf","clear","existing","undefined","next","delete","retrieve","query","total","averageLength","queryTerms","Set","scored","values","filter","matchesFilter","metadata","score","termFrequency","df","idf","Math","log","denominator","push","source","sort","a","slice","topK","toLowerCase","match","LexicalRetriever","store","describeRetrieval","retriever","describeSource","retrieve","query","options","searchText","topK","filter","undefined","HybridRetriever","retrievers","options","describeRetrieval","retriever","describeSharedSource","retrieve","query","k","fetchTopK","weights","lists","Promise","all","map","topK","filter","undefined","fused","Map","forEach","list","listIndex","weight","passage","rank","contribution","existing","get","id","score","set","values","sort","a","b","slice","entry","RerankingRetriever","base","reranker","options","describeRetrieval","retriever","describeSource","retrieve","query","candidates","topK","fetchTopK","filter","undefined","rerank","FilteredRetriever","base","filter","describeRetrieval","retriever","describeSource","retrieve","query","options","chunkDocuments","documents","options","chunks","document","chunkText","text","forEach","index","push","id","source","undefined","metadata","ingestChunks","length","embeddings","embedder","embed","map","chunk","records","embedding","store","upsert","ingestDocuments","RAG_DIAGNOSTIC_LIB","RAG_RETRIEVAL_EVENT","RAG_RETRIEVAL_CHANNEL","channelName","emitRagRetrieval","event","durationMs","emit","DEFAULT_TOP_K","inRetrieval","AsyncLocalStorage","round4","value","Math","round","scoreStats","passages","scores","map","passage","score","filter","Number","isFinite","length","sum","reduce","total","topScore","max","meanScore","InstrumentedRetriever","base","options","describeRetrieval","describe","retrieve","query","getChannel","hasSubscribers","getStore","descriptor","topK","startedAt","Date","now","run","retriever","store","undefined","collectionOf","chunks","zeroHit","failed","error","Error","message","String","collection","resolved","instrumentRetriever","isQueryInput","value","query","querySchema","version","vendor","validate","issues","message","createRetrievalTool","retriever","options","topK","telemetry","instrumented","instrumentRetriever","spec","name","kind","description","inputSchema","handler","execute","input","Error","passages","retrieve","undefined","HttpModelError","Error","status","message","name","openAiEmbeddings","options","url","baseUrl","replace","batchSize","Math","max","floor","maxInputChars","timeoutMs","doFetch","fetch","embed","texts","vectors","start","length","input","slice","map","text","response","method","headers","apiKey","authorization","body","JSON","stringify","model","dimensions","undefined","signal","AbortSignal","timeout","ok","errorMessageOf","json","parseJson","items","data","sort","a","b","index","every","item","Array","isArray","embedding","push","tokens","usage","total_tokens","prompt_tokens","Number","isFinite","onUsage","parse","error","HttpReranker","format","doFetch","options","fetch","rerank","query","passages","length","topK","documents","map","passage","text","response","url","method","headers","apiKey","authorization","body","JSON","stringify","model","undefined","top_n","texts","signal","AbortSignal","timeout","timeoutMs","ok","HttpModelError","status","errorMessageOf","items","itemsOf","seen","Set","reranked","item","index","has","add","push","score","Number","relevance_score","sort","a","b","slice","json","parse","Array","isArray","wrapped","results","data"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/chunk.ts","../src/vector-store.ts","../src/metadata-patch.ts","../src/filter.ts","../src/memory-vector-store.ts","../src/lexical-query.ts","../src/pg-client.ts","../src/pg-vector-store.ts","../src/redis-vector-store.ts","../src/retrieval-descriptor.ts","../src/embedding-retriever.ts","../src/keyword-retriever.ts","../src/lexical-retriever.ts","../src/hybrid-retriever.ts","../src/reranking-retriever.ts","../src/filtered-retriever.ts","../src/ingest.ts","../src/retrieval-telemetry.ts","../src/retrieval-tool.ts","../src/openai-embeddings.ts","../src/http-reranker.ts","../src/tree/types.ts","../src/tree/headings.ts","../src/tree/llm.ts","../src/tree/build.ts","../src/tree/keyword-tree-llm.ts","../src/tree/store.ts","../src/tree/pg-tree-store.ts","../src/tree/index-document.ts","../src/tree/navigate.ts","../src/tree/two-stage.ts","../src/tree/navigate-tool.ts"],"sourcesContent":["export { chunkText, type ChunkOptions } from './chunk.js';\nexport type {\n IndexedDocument,\n LexicalVectorStore,\n ListChunksOptions,\n StoredChunk,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nexport {\n UnsafeRemovalError,\n chunkIndexOf,\n documentIdOf,\n isLexicalVectorStore,\n} from './vector-store.js';\nexport type { MetadataPatch } from './metadata-patch.js';\nexport { applyMetadataPatch } from './metadata-patch.js';\nexport { MemoryVectorStore } from './memory-vector-store.js';\nexport {\n DEFAULT_STOP_WORDS,\n anyTermTsquery,\n hasSearchSyntax,\n keywordTerms,\n} from './lexical-query.js';\nexport {\n PgLexicalVectorStore,\n PgVectorStore,\n type PgFullTextOptions,\n type PgVectorStoreOptions,\n} from './pg-vector-store.js';\nexport {\n toPgClient,\n type DrizzleLike,\n type NodePgQueryable,\n type PgClient,\n type PgClientSource,\n type PostgresJsSql,\n} from './pg-client.js';\nexport {\n RedisVectorStore,\n RedisVectorSchemaMismatchError,\n type RedisSearchClient,\n type RedisVectorStoreOptions,\n} from './redis-vector-store.js';\nexport {\n EmbeddingRetriever,\n type EmbeddingRetrieverOptions,\n} from './embedding-retriever.js';\nexport { KeywordRetriever, type KeywordRetrieverOptions } from './keyword-retriever.js';\nexport { LexicalRetriever } from './lexical-retriever.js';\nexport { HybridRetriever, type HybridRetrieverOptions } from './hybrid-retriever.js';\nexport { RerankingRetriever, type RerankingRetrieverOptions } from './reranking-retriever.js';\nexport { FilteredRetriever } from './filtered-retriever.js';\nexport {\n chunkDocuments,\n ingestChunks,\n ingestDocuments,\n type ChunkRecord,\n type IngestChunksOptions,\n type IngestDocument,\n type IngestOptions,\n} from './ingest.js';\nexport {\n createRetrievalTool,\n type RetrievalTool,\n type RetrievalToolOptions,\n} from './retrieval-tool.js';\nexport {\n describeRetrieval,\n describeSharedSource,\n describeSource,\n describesRetrieval,\n type DescribesRetrieval,\n type RetrievalDescriptor,\n type RetrieverKind,\n type VectorStoreKind,\n} from './retrieval-descriptor.js';\nexport {\n emitRagRetrieval,\n instrumentRetriever,\n RAG_DIAGNOSTIC_LIB,\n RAG_RETRIEVAL_CHANNEL,\n RAG_RETRIEVAL_EVENT,\n type InstrumentRetrieverOptions,\n type RagRetrievalEvent,\n} from './retrieval-telemetry.js';\nexport {\n HttpModelError,\n isBatchTooLarge,\n openAiEmbeddings,\n type OpenAiEmbeddingsOptions,\n} from './openai-embeddings.js';\nexport { HttpReranker, type HttpRerankerOptions } from './http-reranker.js';\nexport * from './tree/index.js';\n","export interface ChunkOptions {\n /** Target max characters per chunk. Default 800. */\n chunkSize?: number;\n /** Characters of overlap carried from the end of one chunk into the next. Default 100. */\n overlap?: number;\n /**\n * Treat the text as records joined by this string, and never split one across chunks.\n *\n * Default chunking is structure-blind: it breaks at the *latest* paragraph/sentence/word boundary\n * in the window, which for record-shaped text means a cut lands mid-record routinely. That is fine\n * for prose, where the two halves are still sentences. It is not fine for a table flattened to one\n * record per row, because the half that keeps the row identifier and the half that keeps the value\n * end up in different chunks, and neither can answer a question about that row on its own.\n *\n * With a separator set, whole records are packed greedily up to `chunkSize` and a record is never\n * cut — except one longer than `chunkSize`, which has nowhere to go and is character-split as\n * usual, since no chunker can avoid that.\n *\n * `overlap` is IGNORED in this mode, deliberately. Overlap exists to rescue a sentence that a\n * boundary cut in half; a boundary that never falls inside a record has nothing to rescue, and\n * carrying the tail of the previous chunk in would duplicate whole records into neighbouring chunks\n * — paying for the same rows twice at embedding time and letting one row match from two places.\n *\n * Measured on a 200-row spreadsheet flattened to one field-labelled record per row (15 columns,\n * ~800-char budget): blind chunking left 27% of records split, and on questions targeting a field\n * on the far side of such a cut, BM25 answered 54/66 (MRR 0.818) against 66/66 (MRR 1.000) with\n * records kept whole. The dense leg scored 7/66 either way — 200 near-identical rows produce\n * near-identical vectors, so that leg cannot do row lookup at all and chunking does not change it.\n */\n separator?: string;\n}\n\n/**\n * Splits text into overlapping chunks for embedding. Greedy up to `chunkSize`, but prefers to break\n * on a paragraph → sentence → word boundary in the back half of the window (so a chunk rarely cuts\n * mid-sentence), then carries `overlap` characters into the next chunk to preserve context across\n * the seam. Pure and deterministic.\n */\nexport function chunkText(text: string, options: ChunkOptions = {}): string[] {\n const chunkSize = options.chunkSize ?? 800;\n const overlap = options.overlap ?? 100;\n const normalized = text.trim();\n if (normalized.length === 0) {\n return [];\n }\n if (options.separator !== undefined && options.separator.length > 0) {\n return chunkByRecord(normalized, chunkSize, options.separator);\n }\n if (normalized.length <= chunkSize) {\n return [normalized];\n }\n\n const chunks: string[] = [];\n let start = 0;\n while (start < normalized.length) {\n const end = Math.min(start + chunkSize, normalized.length);\n let breakAt = end;\n if (end < normalized.length) {\n const window = normalized.slice(start, end);\n const boundary = Math.max(\n window.lastIndexOf('\\n\\n'),\n window.lastIndexOf('. '),\n window.lastIndexOf(' '),\n );\n // Only honour a boundary in the back half, else a tiny chunk cascades.\n if (boundary > chunkSize / 2) {\n breakAt = start + boundary + 1;\n }\n }\n const chunk = normalized.slice(start, breakAt).trim();\n if (chunk.length > 0) {\n chunks.push(chunk);\n }\n if (breakAt >= normalized.length) {\n break;\n }\n // Step forward with overlap, but always make progress (guards against a stuck boundary).\n start = Math.max(breakAt - overlap, start + 1);\n }\n return chunks;\n}\n\n/**\n * Pack whole records up to `chunkSize`, never splitting one. See {@link ChunkOptions.separator}.\n *\n * A record longer than the budget is handed to the blind chunker, which is the honest fallback: it is\n * the same splitting that would have happened anyway, and it is confined to the one record that could\n * not fit rather than applied to the whole document.\n */\nfunction chunkByRecord(text: string, chunkSize: number, separator: string): string[] {\n const chunks: string[] = [];\n let current = '';\n const flush = (): void => {\n if (current.length > 0) {\n chunks.push(current);\n current = '';\n }\n };\n for (const record of text.split(separator)) {\n const trimmed = record.trim();\n if (trimmed.length === 0) {\n continue;\n }\n if (trimmed.length > chunkSize) {\n flush();\n // `separator: undefined` on purpose — this is the blind path, for the one record that cannot\n // fit whole. Overlap stays off so the fallback does not reintroduce duplication the mode exists\n // to avoid.\n chunks.push(...chunkText(trimmed, { chunkSize, overlap: 0 }));\n continue;\n }\n const candidate = current.length > 0 ? `${current}${separator}${trimmed}` : trimmed;\n if (candidate.length > chunkSize) {\n flush();\n current = trimmed;\n } else {\n current = candidate;\n }\n }\n flush();\n return chunks;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport type { MetadataPatch } from './metadata-patch.js';\n\n/** A stored, embedded chunk. `embedding` length must match the store's configured dimensions. */\nexport interface VectorRecord {\n id: string;\n text: string;\n embedding: number[];\n /** Citation-facing origin (document title, URL, row id). */\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface VectorSearchOptions {\n topK: number;\n /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */\n filter?: Record<string, unknown>;\n}\n\n/**\n * The write + search side of RAG storage. `@dudousxd/nestjs-agent-rag` ships `MemoryVectorStore`\n * (in-JS cosine, tests + small scale) and `PgVectorStore` (pgvector); any impl works. Pair one with\n * an `EmbeddingProvider` via {@link import('./embedding-retriever.js').EmbeddingRetriever} to get a\n * core `Retriever`.\n *\n * Every method here is **required**, the enumeration and bulk-deletion half included. The rule for\n * where a new method goes, so the next person adding one has it rather than inferring it from\n * precedent:\n *\n * - **Required, on this interface**, when every backend *can* implement it from what it already\n * stores. {@link VectorStore.updateMetadata}, {@link VectorStore.listDocumentIds},\n * {@link VectorStore.removeMany}, {@link VectorStore.removeWhere} and\n * {@link VectorStore.countChunks} are CRUD over the records the store already holds — the same\n * footing as {@link VectorStore.remove} and\n * {@link VectorStore.listDocuments}. Making them optional would buy nothing but a permanent\n * `if (supported)` at every call site plus an unreachable \"and if it isn't?\" branch.\n * - **Optional, in a capability interface**, when some backend genuinely *cannot* without\n * infrastructure the consumer has to adopt. The only such capability today is\n * {@link LexicalVectorStore.searchText}: RediSearch has BM25 natively, Postgres would need a\n * `tsvector` column plus a GIN index plus a migration the consumer must run, and\n * `MemoryVectorStore` would have to implement BM25 itself.\n *\n * The test is whether a backend can do it at all — not whether adding it would inconvenience an\n * implementer who has already shipped.\n *\n * Implementations MUST apply every filter with exactly the same semantics as\n * {@link VectorStore.search} — including the empty-array deny primitive — because the filter is\n * routinely an access-control boundary, not a hint. On {@link VectorStore.removeWhere} that is not a\n * correctness nicety but the single most destructive thing an implementation could get wrong.\n */\nexport interface VectorStore {\n upsert(records: VectorRecord[]): Promise<void>;\n search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;\n /**\n * Delete every chunk belonging to a source document — all records whose id is `${documentId}`\n * or `${documentId}#<n>` (the id scheme {@link import('./ingest.js').chunkDocuments} produces).\n * Use it to drop a document from the index, and before re-ingesting one: `upsert` overwrites\n * matching ids but can't remove chunks a shorter new version no longer produces, so a re-ingest\n * without a preceding `remove` leaves the old tail orphaned.\n */\n remove(documentId: string): Promise<void>;\n /**\n * Rewrite a document's metadata **without touching its text or its embeddings** — the update path\n * for a dimension that is genuinely mutable (which bases a document is relevant to, who may see it,\n * what it was reclassified as) rather than derived from its content.\n *\n * Without this, a consumer whose documents get re-classified has exactly two options, and both are\n * bad: re-embed the whole document — paying the model bill, and burning the ingestion budget, to\n * change a string — or refuse to stamp the dimension onto chunks at all and resolve it at query\n * time instead, which turns a filter the index could have applied into a join the caller has to.\n *\n * `patch` is applied to **every chunk** of the document (metadata is a document-level property that\n * happens to be stored per chunk), with the merge semantics documented on {@link MetadataPatch}:\n * partial merge, `null` to remove a key, `undefined` ignored, values — including arrays — replaced\n * wholesale.\n *\n * Returns **the number of chunks written**. An unknown `documentId` updates nothing and returns\n * `0`; it does not throw. That is deliberate and matches {@link VectorStore.remove}, which is\n * likewise silent on a document that isn't there: this is a reconciliation-shaped API, called by a\n * loop diffing a source of truth against the index, and such a loop races with ingestion and\n * deletion by construction. Turning \"the document was removed between the diff and the write\" into\n * an exception would make the ordinary case an error, and the caller would have to catch and\n * swallow it to get back the behaviour it wanted. The count gives the same information without\n * that: `0` means nothing was there. (An empty patch also writes nothing and returns `0` — the\n * return counts chunks *written*, not chunks *matched*.)\n *\n * Implementations MUST keep every representation of the metadata in step. A store that keeps both\n * a filterable projection and a returned blob (RediSearch's `meta_*` TAGs beside its\n * `metadata_json`) and updates only one makes the store lie to itself: a chunk that *filters* as\n * one value but *reports* another, where which half you notice depends on whether you searched or\n * read.\n */\n updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;\n /**\n * List the distinct source documents currently indexed (chunk ids collapsed back to their document\n * by stripping the trailing `#<n>`), each with a representative chunk's `metadata`, optionally\n * narrowed by a metadata `filter`. The enumeration seam for reconciliation: diff this against your\n * source of truth to find documents to ingest (missing), re-ingest (a stored fingerprint like\n * `size` changed), or {@link VectorStore.remove remove} (orphaned).\n */\n listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;\n\n /**\n * Read one document's chunks back — id, text, metadata — ordered by their position in the\n * document, optionally paged with `limit`/`offset`.\n *\n * The gap this closes is that **the text a store holds is otherwise write-only**. Every other way\n * back to it is a ranked query: {@link VectorStore.search} needs an embedding,\n * {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,\n * in relevance order, capped at `topK`. Neither can answer \"show me what was indexed for this\n * document, in order\" — the question you ask when retrieval returns the wrong passage and you need\n * to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to\n * see stored text is to reach around the adapter into the backing engine, which is exactly the\n * abstraction break a consumer adopts a store to avoid.\n *\n * Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and\n * {@link VectorStore.updateMetadata}: ordering is the whole point, and \"position within the\n * document\" is only meaningful once a document has been picked. Enumerate documents with\n * {@link VectorStore.listDocuments} first when you need to sweep.\n *\n * An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.\n *\n * NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce\n * one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST\n * resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately\n * shaped for an operator/debug surface, not for serving end users.\n */\n listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;\n\n /**\n * The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —\n * {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a\n * reconciliation diff against your source of truth, a count, a delete list): implementations are\n * expected to skip fetching and parsing per-chunk metadata entirely, which is most of the cost of\n * `listDocuments` on a large corpus.\n *\n * An empty-array filter value denies, yielding `[]`.\n */\n listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;\n\n /**\n * Delete every chunk of every one of `documentIds`, with the same per-document semantics as\n * {@link VectorStore.remove} — but as one bulk operation rather than N independent ones. An empty\n * array is a no-op.\n *\n * The gap this closes is asymptotic, not cosmetic: on the Redis adapter each `remove` is a `SCAN`\n * that walks the whole keyspace, which in a real deployment also holds queues, workflow state and\n * the app cache. Two thousand documents is two thousand full passes; this is one filtered query.\n */\n removeMany(documentIds: string[]): Promise<void>;\n\n /**\n * Delete every chunk matching a metadata `filter`, and resolve the number of **chunks** removed\n * (not documents). The collection-maintenance primitive: dropping a collection, evicting a tenant,\n * purging a retired audience.\n *\n * Two guard rails, because this is the one method here that destroys data:\n *\n * - **The empty-array deny is honoured, and it means \"delete nothing\".** `removeWhere({ audience: [] })`\n * removes zero chunks. Treating a deny as \"no filter\" would delete the entire corpus, which is\n * exactly inverted from what the caller asked for.\n * - **The filter must be non-empty.** `removeWhere({})` throws {@link UnsafeRemovalError} rather\n * than wiping the store, because an empty object is overwhelmingly more likely to be a filter that\n * got built wrong (a dropped key, an `undefined` scope) than a deliberate request to delete\n * everything. Deliberate mass deletion stays available and stays explicit:\n * `removeMany(await store.listDocumentIds())`, or the backend's own drop.\n *\n * What it removes is exactly what a {@link VectorStore.search} carrying the same filter could\n * reach — no more (it cannot escape the filter) and no less.\n */\n removeWhere(filter: Record<string, unknown>): Promise<number>;\n\n /**\n * How many **chunks** match `filter` (all of them when it is omitted) — a counted query, without\n * transferring the chunks. Sizing a reindex, reporting a collection's footprint, and confirming a\n * {@link VectorStore.removeWhere} did what you expected. An empty-array filter value denies,\n * yielding `0`.\n */\n countChunks(filter?: Record<string, unknown>): Promise<number>;\n}\n\n/**\n * An **optional** capability a {@link VectorStore} may also implement: full-text (lexical) search\n * over the chunk text it already stores, using the backing engine's own keyword index — RediSearch's\n * BM25 over the `TEXT` field, say. Its point is the multi-process deployment: when ingestion runs on\n * a worker and search runs on an API pod, the API process never sees the chunks, so the in-process\n * {@link import('./keyword-retriever.js').KeywordRetriever} has nothing to index. A store that\n * implements this needs no second index, no corpus in JS heap, and no refresh window — a chunk is\n * lexically findable the moment it is upserted.\n *\n * Wrap it as a core `Retriever` with {@link import('./lexical-retriever.js').LexicalRetriever}.\n *\n * Implementations MUST apply `options.filter` with exactly the same semantics as\n * {@link VectorStore.search} — including the empty-array deny primitive — because the filter is\n * routinely an access-control boundary, not a hint.\n */\nexport interface LexicalVectorStore extends VectorStore {\n /**\n * Rank stored chunks against a natural-language `query` by keyword relevance. `options.filter`\n * narrows exactly as in {@link VectorStore.search}. The returned `score` is the engine's own\n * relevance score (BM25), which shares no scale with `search`'s cosine similarity — fuse the two\n * with {@link import('./hybrid-retriever.js').HybridRetriever}, whose RRF is rank-based and so\n * needs no common scale.\n */\n searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;\n}\n\n/**\n * Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the\n * store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)\n * when it is not, without knowing which adapter it was handed.\n */\nexport function isLexicalVectorStore(store: VectorStore): store is LexicalVectorStore {\n return typeof (store as Partial<LexicalVectorStore>).searchText === 'function';\n}\n\n/**\n * A destructive call was refused because its scope could not be trusted. Thrown by\n * {@link VectorStore.removeWhere} — never for something that merely matched nothing (that\n * is a legitimate result of `0`), only for a filter that would have deleted *more* than the caller\n * plausibly meant.\n */\nexport class UnsafeRemovalError extends Error {\n constructor(\n /** Why the removal was refused. */\n readonly reason: 'empty-filter' | 'unindexed-field',\n message: string,\n ) {\n super(message);\n this.name = 'UnsafeRemovalError';\n }\n}\n\n/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */\nexport interface IndexedDocument {\n id: string;\n metadata?: Record<string, unknown>;\n}\n\n/** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */\nexport interface StoredChunk {\n /** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */\n id: string;\n /** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */\n index: number;\n /** The chunk text as it was indexed — the exact string the embedding was computed from. */\n text: string;\n metadata?: Record<string, unknown>;\n}\n\n/** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */\nexport interface ListChunksOptions {\n /** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */\n limit?: number;\n /** Chunks to skip, applied after ordering. */\n offset?: number;\n}\n\n/** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */\nexport function chunkIndexOf(chunkId: string): number {\n const match = /#(\\d+)$/.exec(chunkId);\n return match?.[1] === undefined ? 0 : Number(match[1]);\n}\n\n/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */\nexport function documentIdOf(chunkId: string): string {\n return chunkId.replace(/#\\d+$/, '');\n}\n","/**\n * A partial update to a document's metadata, as taken by\n * {@link import('./vector-store.js').VectorStore.updateMetadata}. Deliberately *partial*: the useful\n * shape is \"this one dimension was reclassified\", not \"here is the whole metadata object again\" —\n * a caller that had the whole object would have to reconstruct fields it does not own (the ingestion\n * fingerprint, the source, the mime type) just to change one string, and would silently drop them\n * the day the library adds another.\n *\n * The semantics are RFC 7386 (JSON Merge Patch), with one deliberate narrowing:\n *\n * - **Merge, not replace.** Keys present in the patch are written; keys absent from it keep their\n * stored value.\n * - **`null` removes the key.** That is the RFC's sentinel and it is the *only* way to remove one.\n * The consequence — a literal `null` cannot be stored as a value — is intended: metadata is a\n * filter surface, and \"the key is absent\" and \"the key is null\" would filter identically anyway.\n * - **`undefined` is ignored** — the key is left exactly as stored, as if the patch had not mentioned\n * it. `undefined` does not survive `JSON.stringify`, and a patch routinely crosses a JSON boundary\n * (an HTTP body, a durable-workflow step payload) on its way here; honouring it would make\n * `{ audience: undefined }` mean *delete* on one side of that boundary and *no-op* on the other.\n * It is also what `{ ...doc, audience: doc.audience }` produces by accident, and an accidental\n * deletion of an access-control dimension is the worst outcome this API can have. Removing a key\n * is therefore always spelled `null`.\n * - **Shallow.** A value replaces wholesale; it is not merged into the stored value. This is the\n * narrowing versus RFC 7386, which recurses into nested objects. It matters most for **arrays**,\n * where wholesale replacement is certainly what a caller means: `{ bases: ['A', 'C'] }` sets the\n * document's bases to exactly A and C — it does not append to, or union with, what was there. An\n * array-valued key is a *set-valued dimension* (the multi-valued TAG a store filters on), and the\n * only sane way to shrink such a set is to state the new one.\n */\nexport type MetadataPatch = Record<string, unknown>;\n\n/**\n * Split a patch into the assignments it makes and the keys it removes — the shape a store that\n * updates metadata declaratively (SQL, a hash write + field delete) needs, since setting and\n * unsetting are different operations there. `undefined` values appear in neither.\n */\nexport function splitMetadataPatch(patch: MetadataPatch): {\n set: Record<string, unknown>;\n remove: string[];\n} {\n const set: Record<string, unknown> = {};\n const remove: string[] = [];\n for (const [key, value] of Object.entries(patch)) {\n if (value === null) {\n remove.push(key);\n } else if (value !== undefined) {\n set[key] = value;\n }\n }\n return { set, remove };\n}\n\n/** Does this patch write anything at all? An empty (or all-`undefined`) patch is a no-op. */\nexport function isEmptyMetadataPatch(patch: MetadataPatch): boolean {\n const { set, remove } = splitMetadataPatch(patch);\n return Object.keys(set).length === 0 && remove.length === 0;\n}\n\n/**\n * Apply a {@link MetadataPatch} to a document's stored metadata, returning a NEW object — the\n * reference implementation of the semantics documented on `MetadataPatch`, so every adapter that\n * merges in JS agrees with every other one. Never mutates its input: `MemoryVectorStore` hands out\n * the very object a caller upserted, and quietly rewriting it under them would be a surprise no\n * other store reproduces.\n */\nexport function applyMetadataPatch(\n metadata: Record<string, unknown> | undefined,\n patch: MetadataPatch,\n): Record<string, unknown> {\n const { set, remove } = splitMetadataPatch(patch);\n const next: Record<string, unknown> = { ...metadata, ...set };\n for (const key of remove) {\n delete next[key];\n }\n return next;\n}\n","/** Metadata filter, shared by the in-memory {@link import('./memory-vector-store.js').MemoryVectorStore}\n * and {@link import('./keyword-retriever.js').KeywordRetriever}. Every key must match:\n *\n * - a **scalar** filter value keeps exact-match — the record's value must equal it;\n * - an **array** filter value means **match-any** (OR / set membership) — the record matches when its\n * value for that key is one of them, or (for a multi-valued record) shares at least one element.\n * An empty array matches nothing (no value is a member of the empty set) — the deny primitive.\n *\n * Array-valued filters are how a caller expresses capability-style access control over a shared store\n * (e.g. `{ audience: ['public', 'role:ADMIN', 'base:…'] }`) without the store knowing what a token means.\n */\nexport function matchesFilter(\n metadata: Record<string, unknown> | undefined,\n filter: Record<string, unknown>,\n): boolean {\n const entries = Object.entries(filter);\n if (metadata === undefined) {\n return entries.length === 0;\n }\n return entries.every(([key, expected]) => {\n if (Array.isArray(expected)) {\n const actual = metadata[key];\n return Array.isArray(actual)\n ? actual.some((value) => expected.includes(value))\n : expected.includes(actual);\n }\n return metadata[key] === expected;\n });\n}\n\n/**\n * Does this filter provably match **nothing**? True when any key carries an empty array, because\n * nothing is a member of the empty set and every key must match (AND). This is the package's **deny\n * primitive** — the shape a capability-style ACL collapses to for an actor holding no tokens — so\n * every store has to honour it identically, and a store that cannot express \"match nothing\" in its\n * query language (RediSearch has no valid empty-tag syntax) must short-circuit on it instead.\n *\n * It is exported from this module rather than reimplemented per adapter precisely because getting it\n * wrong is *silent*: a read path that ignores it over-returns, and a **write** path that ignores it\n * (see {@link import('./vector-store.js').VectorStore.removeWhere}) deletes the corpus a\n * deny was supposed to protect.\n */\nexport function filterMatchesNothing(filter?: Record<string, unknown>): boolean {\n if (filter === undefined) {\n return false;\n }\n return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing, matchesFilter } from './filter.js';\nimport { type MetadataPatch, applyMetadataPatch, isEmptyMetadataPatch } from './metadata-patch.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport {\n type IndexedDocument,\n type ListChunksOptions,\n type StoredChunk,\n UnsafeRemovalError,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n chunkIndexOf,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter\n * for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).\n *\n * It implements the enumeration and bulk-deletion half of {@link VectorStore} too. There is no\n * performance argument for it here — every method below is a loop over the same Map — but a store that tests are\n * written against has to behave identically to the one production runs, and the semantics that matter\n * (`removeWhere` refusing an empty filter, an empty-array filter deleting nothing) are behavioural,\n * not incidental to Redis.\n */\nexport class MemoryVectorStore implements VectorStore {\n private readonly records = new Map<string, VectorRecord>();\n\n /**\n * Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this\n * instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them\n * apart on a dashboard.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'memory' };\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n for (const id of this.records.keys()) {\n if (documentIdOf(id) === documentId) {\n this.records.delete(id);\n }\n }\n }\n\n /**\n * Merge `patch` into every chunk of `documentId`, leaving `text` and `embedding` untouched. The\n * record is *replaced* rather than mutated: `upsert` stores the caller's own object, and patching\n * it in place would edit an object the caller still holds — a surprise no out-of-process store\n * could reproduce, and therefore one this reference adapter must not teach.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n let updated = 0;\n for (const [id, record] of this.records) {\n if (documentIdOf(id) !== documentId) {\n continue;\n }\n this.records.set(id, { ...record, metadata: applyMetadataPatch(record.metadata, patch) });\n updated += 1;\n }\n return updated;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const documents = new Map<string, IndexedDocument>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n const id = documentIdOf(record.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return [...documents.values()];\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * Insertion order is not document order — `upsert` stores whatever the caller passed, and a\n * re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index\n * parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the\n * Map. A reference adapter that returned \"usually ordered\" would let a consumer ship a bug that\n * only shows up on the store tests are NOT written against.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n const chunks: StoredChunk[] = [];\n for (const record of this.records.values()) {\n if (documentIdOf(record.id) !== documentId) {\n continue;\n }\n chunks.push({\n id: record.id,\n index: chunkIndexOf(record.id),\n text: record.text,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n chunks.sort((a, b) => a.index - b.index);\n const offset = options?.offset ?? 0;\n return options?.limit === undefined\n ? chunks.slice(offset)\n : chunks.slice(offset, offset + options.limit);\n }\n\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const documents = new Set<string>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n documents.add(documentIdOf(record.id));\n }\n return [...documents];\n }\n\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n const wanted = new Set(documentIds);\n for (const id of this.records.keys()) {\n if (wanted.has(documentIdOf(id))) {\n this.records.delete(id);\n }\n }\n }\n\n /**\n * See {@link VectorStore.removeWhere}. The empty-array short-circuit below is redundant\n * against `matchesFilter` (which already matches nothing for an empty array) and is kept anyway:\n * this is the method where \"the filter accidentally means everything\" destroys data, so the deny is\n * stated where a reader can see it rather than inferred from another module's truth table.\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n if (Object.keys(filter).length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the store. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n let removed = 0;\n for (const [id, record] of this.records) {\n if (matchesFilter(record.metadata, filter)) {\n this.records.delete(id);\n removed += 1;\n }\n }\n return removed;\n }\n\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n let count = 0;\n for (const record of this.records.values()) {\n if (filter === undefined || matchesFilter(record.metadata, filter)) {\n count += 1;\n }\n }\n return count;\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const scored: Passage[] = [];\n for (const record of this.records.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n scored.push({\n id: record.id,\n text: record.text,\n score: cosineSimilarity(embedding, record.embedding),\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK);\n }\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n const length = Math.min(a.length, b.length);\n for (let index = 0; index < length; index += 1) {\n const valueA = a[index] ?? 0;\n const valueB = b[index] ?? 0;\n dot += valueA * valueB;\n normA += valueA * valueA;\n normB += valueB * valueB;\n }\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dot / denominator;\n}\n","/**\n * Turning a natural-language question into full-text search terms, for the lexical leg of\n * `PgLexicalVectorStore`.\n *\n * With the `simple` text-search configuration (no stemming, no stop words; any language) a question\n * searched as is either matches nothing (every word required) or, as \"any word\", ranks chunks full\n * of \"the / of / which / de / que\" first. So the query side drops the stop words of the question's\n * language(s) and keeps the meaningful terms, which the store then ranks by how many of them a chunk\n * holds, rare terms weighing more (an IDF over the matching rows, BM25-style).\n */\n\nconst words = (s: string) => new Set(s.split(/\\s+/).filter(Boolean));\n\n/**\n * Postgres' Snowball stop word lists (`tsearch_data/{english,portuguese,spanish}.stop`), minus\n * one-letter words (never terms here) and words that carry meaning in a sibling language\n * (\"estado\", \"sentido\").\n */\nexport const DEFAULT_STOP_WORDS: Readonly<Record<string, ReadonlySet<string>>> = {\n english: words(`\n me my myself we our ours ourselves you your yours yourself yourselves he him his himself she\n her hers herself it its itself they them their theirs themselves what which who whom this that\n these those am is are was were be been being have has had having do does did doing an the and\n but if or because as until while of at by for with about against between into through during\n before after above below to from up down in out on off over under again further then once here\n there when where why how all any both each few more most other some such no nor not only own\n same so than too very can will just don should now`),\n portuguese: words(`\n de que do da em um para com não uma os no se na por mais as dos como mas ao ele das seu sua ou\n quando muito nos já eu também só pelo pela até isso ela entre depois sem mesmo aos seus quem\n nas me esse eles você essa num nem suas meu às minha numa pelos elas qual nós lhe deles essas\n esses pelas este dele tu te vocês vos lhes meus minhas teu tua teus tuas nosso nossa nossos\n nossas dela delas esta estes estas aquele aquela aqueles aquelas isto aquilo estou está estamos\n estão estive esteve estivemos estiveram estava estávamos estavam estivera estivéramos esteja\n estejamos estejam estivesse estivéssemos estivessem estiver estivermos estiverem hei há havemos\n hão houve houvemos houveram houvera houvéramos haja hajamos hajam houvesse houvéssemos\n houvessem houver houvermos houverem houverei houverá houveremos houverão houveria houveríamos\n houveriam sou somos são era éramos eram fui foi fomos foram fora fôramos seja sejamos sejam\n fosse fôssemos fossem for formos forem serei será seremos serão seria seríamos seriam tenho tem\n temos tém tinha tínhamos tinham tive teve tivemos tiveram tivera tivéramos tenha tenhamos\n tenham tivesse tivéssemos tivessem tiver tivermos tiverem terei terá teremos terão teria\n teríamos teriam`),\n spanish: words(`\n de la que el en los del se las por un para con no una su al lo como más pero sus le ya este sí\n porque esta entre cuando muy sin sobre también me hasta hay donde quien desde todo nos durante\n todos uno les ni contra otros ese eso ante ellos esto mí antes algunos qué unos yo otro otras\n otra él tanto esa estos mucho quienes nada muchos cual poco ella estar estas algunas algo\n nosotros mi mis tú te ti tu tus ellas nosotras vosotros vosotras os mío mía míos mías tuyo\n tuya tuyos tuyas suyo suya suyos suyas nuestro nuestra nuestros nuestras vuestro vuestra\n vuestros vuestras esos esas estoy estás está estamos estáis están esté estés estemos estéis\n estén estaré estarás estará estaremos estaréis estarán estaría estarías estaríamos estaríais\n estarían estaba estabas estábamos estabais estaban estuve estuviste estuvo estuvimos\n estuvisteis estuvieron estuviera estuvieras estuviéramos estuvierais estuvieran estuviese\n estuvieses estuviésemos estuvieseis estuviesen estando estad he has ha hemos habéis han haya\n hayas hayamos hayáis hayan habré habrás habrá habremos habréis habrán habría habrías habríamos\n habríais habrían había habías habíamos habíais habían hube hubiste hubo hubimos hubisteis\n hubieron hubiera hubieras hubiéramos hubierais hubieran hubiese hubieses hubiésemos hubieseis\n hubiesen habiendo habido habida habidos habidas soy eres es somos sois son sea seas seamos\n seáis sean seré serás será seremos seréis serán sería serías seríamos seríais serían era eras\n éramos erais eran fui fuiste fue fuimos fuisteis fueron fuera fueras fuéramos fuerais fueran\n fuese fueses fuésemos fueseis fuesen tengo tienes tiene tenemos tenéis tienen tenga tengas\n tengamos tengáis tengan tendré tendrás tendrá tendremos tendréis tendrán tendría tendrías\n tendríamos tendríais tendrían tenía tenías teníamos teníais tenían tuve tuviste tuvo tuvimos\n tuvisteis tuvieron tuviera tuvieras tuviéramos tuvierais tuvieran tuviese tuvieses tuviésemos\n tuvieseis tuviesen teniendo tenido tenida tenidos tenidas tened`),\n};\n\nconst TOKEN = /[\\p{L}\\p{N}]{2,}/gu;\n\n/**\n * The meaningful terms of a question, in order, deduplicated, at most `max`: the stop words of the\n * language(s) the question is written in are dropped (the language(s) with the most stop words in\n * it, so \"sea\"/\"son\" stay terms in an English question although they are Spanish stop words). A\n * question made only of stop words keeps them all (there is nothing else to search for).\n */\nexport function keywordTerms(\n query: string,\n max = 24,\n stopWords: Readonly<Record<string, ReadonlySet<string>>> = DEFAULT_STOP_WORDS,\n): string[] {\n const tokens = [...new Set(query.toLowerCase().normalize('NFC').match(TOKEN) ?? [])];\n const counts = Object.entries(stopWords).map(\n ([, stop]) => [stop, tokens.filter((t) => stop.has(t)).length] as const,\n );\n const best = Math.max(0, ...counts.map(([, n]) => n));\n if (best === 0) return tokens.slice(0, max);\n const stop = counts.filter(([, n]) => n === best).map(([s]) => s);\n const terms = tokens.filter((t) => !stop.some((s) => s.has(t)));\n return (terms.length > 0 ? terms : tokens).slice(0, max);\n}\n\n/** Whether the user wrote search syntax (a quoted phrase, `-exclusion`) worth honoring as such. */\nexport function hasSearchSyntax(query: string): boolean {\n return /\"[^\"]+\"/.test(query) || /(^|\\s)-[\\p{L}\\p{N}]/u.test(query);\n}\n\n/** `'a' | 'b' | …` for `to_tsquery` (terms are letter/digit runs, so quoting is enough). */\nexport function anyTermTsquery(terms: string[]): string {\n return terms.map((t) => `'${t.replace(/'/g, '')}'`).join(' | ');\n}\n","/**\n * The minimal Postgres surface {@link import('./pg-vector-store.js').PgVectorStore} needs — adapt your\n * own `pg` / `postgres.js` client to it, so this package pulls in NO driver (bring your own, like the\n * store adapters take an ORM handle and the Redis transport takes a client). `query` runs\n * parameterized SQL (`$1,$2,…`) and resolves the result rows.\n *\n * `transaction` is optional. When present, the store uses it for the few reads that must share one\n * connection with a `SET LOCAL` (pgvector's iterative index scan); without it those settings are\n * simply not applied. You rarely write either by hand: pass a `pg` `Pool`, a Drizzle database or a\n * `postgres.js` `sql` and {@link toPgClient} builds both.\n */\nexport interface PgClient {\n query<Row = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<Row[]>;\n /** Run `work` inside one transaction on one connection; commit on success, roll back on throw. */\n transaction?<T>(work: (tx: PgClient) => Promise<T>): Promise<T>;\n}\n\n/**\n * A node-postgres `Pool`, `PoolClient` or `Client` — anything whose `query(text, values)` resolves a\n * result with `rows`. Typed structurally so this package needs no `pg` import.\n */\nexport interface NodePgQueryable {\n query(text: string, values?: any[]): Promise<unknown>;\n}\n\n/** A `postgres.js` `sql` instance (or a transaction handle from `sql.begin`). */\nexport interface PostgresJsSql {\n unsafe(query: string, params?: any[]): PromiseLike<unknown>;\n begin?(...args: any[]): PromiseLike<unknown>;\n}\n\n/**\n * A Drizzle database — `drizzle(pool)` for node-postgres or `drizzle(sql)` for postgres.js. Drizzle\n * exposes the driver it wraps as `$client`, which is what the store talks to; this saves the\n * `(db as unknown as { $client: Pool }).$client` cast at every call site.\n */\nexport interface DrizzleLike {\n $client: NodePgQueryable | PostgresJsSql;\n}\n\n/** Everything {@link toPgClient} (and so the `PgVectorStore` constructor) accepts. */\nexport type PgClientSource = PgClient | NodePgQueryable | PostgresJsSql | DrizzleLike;\n\nconst ADAPTED = Symbol.for('@dudousxd/nestjs-agent-rag/pg-client');\n\n/**\n * Normalize a Postgres handle into a {@link PgClient}:\n *\n * - a Drizzle database → its `$client`, then one of the below;\n * - a `postgres.js` `sql` → `sql.unsafe(text, params)`, transactions via `sql.begin`;\n * - a node-postgres `Pool` → `pool.query(...).rows`, transactions on a checked-out client\n * (`BEGIN`/`COMMIT`/`ROLLBACK`, then `release()`); a single `Client`/`PoolClient` runs its\n * transaction in place;\n * - a hand-written {@link PgClient} (`query` resolving the rows array) passes through unchanged.\n *\n * Detection is structural and happens per call result for `query` (an array is taken as the rows, an\n * object's `rows` otherwise), so a hand-written adapter keeps working exactly as before.\n */\nexport function toPgClient(source: PgClientSource): PgClient {\n if ((source as { [ADAPTED]?: true })[ADAPTED]) {\n return source as PgClient;\n }\n if (isDrizzle(source)) {\n return toPgClient(source.$client);\n }\n if (typeof (source as PostgresJsSql).unsafe === 'function') {\n return fromPostgresJs(source as PostgresJsSql);\n }\n return fromQueryable(source as NodePgQueryable & Partial<PgClient>);\n}\n\nfunction isDrizzle(source: PgClientSource): source is DrizzleLike {\n const client = (source as Partial<DrizzleLike>).$client;\n return client !== null && client !== undefined && typeof client === 'object';\n}\n\nfunction mark(client: PgClient): PgClient {\n Object.defineProperty(client, ADAPTED, { value: true });\n return client;\n}\n\nfunction rowsOf<Row>(result: unknown): Row[] {\n if (Array.isArray(result)) {\n return result as Row[];\n }\n const rows = (result as { rows?: unknown } | null | undefined)?.rows;\n return Array.isArray(rows) ? (rows as Row[]) : [];\n}\n\nfunction fromPostgresJs(sql: PostgresJsSql): PgClient {\n const client: PgClient = {\n query: async <Row>(text: string, params: unknown[] = []) =>\n // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.\n [...rowsOf<Row>(await sql.unsafe(text, params))],\n };\n if (typeof sql.begin === 'function') {\n const begin = sql.begin.bind(sql);\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) =>\n Promise.resolve(begin((tx: PostgresJsSql) => work(fromPostgresJs(tx)))) as Promise<T>;\n }\n return mark(client);\n}\n\ninterface NodePgPoolLike extends NodePgQueryable {\n connect(): Promise<NodePgQueryable & { release(error?: unknown): void }>;\n totalCount: number;\n}\n\nfunction fromQueryable(source: NodePgQueryable & Partial<PgClient>): PgClient {\n const client: PgClient = {\n query: async <Row>(text: string, params?: unknown[]) =>\n rowsOf<Row>(await source.query(text, params)),\n };\n if (typeof source.transaction === 'function') {\n // A hand-written PgClient that already knows how to run a transaction.\n const transaction = source.transaction.bind(source);\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) =>\n transaction((tx: PgClient) => work(toPgClient(tx)));\n } else if (isNodePgPool(source)) {\n client.transaction = async <T>(work: (tx: PgClient) => Promise<T>) => {\n const connection = await source.connect();\n let broken = false;\n try {\n return await runInTransaction(connection, work, () => {\n broken = true;\n });\n } finally {\n // A connection whose ROLLBACK failed is not safe to hand back to the pool: a truthy\n // argument makes node-postgres destroy it instead.\n connection.release(broken ? new Error('ROLLBACK failed') : undefined);\n }\n };\n } else if (isNodePgClient(source)) {\n client.transaction = <T>(work: (tx: PgClient) => Promise<T>) => runInTransaction(source, work);\n }\n return mark(client);\n}\n\nasync function runInTransaction<T>(\n connection: NodePgQueryable,\n work: (tx: PgClient) => Promise<T>,\n onRollbackFailed?: () => void,\n): Promise<T> {\n const tx = mark({\n query: async <Row>(text: string, params?: unknown[]) =>\n rowsOf<Row>(await connection.query(text, params)),\n });\n await connection.query('BEGIN');\n try {\n const result = await work(tx);\n await connection.query('COMMIT');\n return result;\n } catch (error) {\n // The original error is what the caller needs; a failed ROLLBACK only poisons the connection.\n await connection.query('ROLLBACK').catch(() => onRollbackFailed?.());\n throw error;\n }\n}\n\nfunction isNodePgPool(source: object): source is NodePgPoolLike {\n return (\n typeof (source as Partial<NodePgPoolLike>).connect === 'function' &&\n typeof (source as Partial<NodePgPoolLike>).totalCount === 'number'\n );\n}\n\n/** A node-postgres `Client`/`PoolClient`: one connection, so a transaction runs on it directly. */\nfunction isNodePgClient(source: object): boolean {\n return (\n typeof (source as { release?: unknown }).release === 'function' ||\n typeof (source as { processID?: unknown }).processID === 'number' ||\n (source as { connectionParameters?: unknown }).connectionParameters !== undefined\n );\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing } from './filter.js';\nimport {\n DEFAULT_STOP_WORDS,\n anyTermTsquery,\n hasSearchSyntax,\n keywordTerms,\n} from './lexical-query.js';\nimport { type MetadataPatch, isEmptyMetadataPatch, splitMetadataPatch } from './metadata-patch.js';\nimport { type PgClient, type PgClientSource, toPgClient } from './pg-client.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport type {\n IndexedDocument,\n LexicalVectorStore,\n ListChunksOptions,\n StoredChunk,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nimport { UnsafeRemovalError } from './vector-store.js';\n\nexport type { PgClient } from './pg-client.js';\n\n/** Where the lexical leg of a {@link PgLexicalVectorStore} reads its `tsvector` from. */\nexport interface PgFullTextOptions {\n /**\n * Text-search configuration (`simple`, `english`, `portuguese`, …). Default `simple` — no stemming\n * or stop words, so it works for any language and matches exact terms, which is what the lexical\n * leg of a hybrid search is for. Must be the same configuration the index was built with, or\n * Postgres cannot use the index and scans the table (still correct, just slow).\n */\n config?: string;\n /**\n * A `tsvector` column you maintain yourself (typically `GENERATED ALWAYS AS (…) STORED` in a\n * migration, e.g. weighting a title above the body), with its own GIN index. When omitted the\n * store searches the expression `to_tsvector(config, text)`, and `ensureSchema` creates a GIN\n * expression index over exactly that.\n */\n column?: string;\n /**\n * Search natural-language questions by their meaningful terms (default `true`): the question's\n * stop words are dropped (see {@link PgFullTextOptions.stopWords}) and rows holding any remaining\n * term (at most 24) are ranked by the sum of the matched terms' IDF among those rows (BM25\n * without term frequencies), `ts_rank_cd` breaking ties; a question with explicit search syntax\n * (a quoted phrase, `-word`) is first tried as written. `false`: only `websearch_to_tsquery` on\n * the query as written (every word must appear).\n */\n anyTermFallback?: boolean;\n /**\n * Stop word lists dropped from questions, by language; only the list(s) with the most words in\n * the question apply, so a word that is a stop word in another language stays a term. Default\n * {@link DEFAULT_STOP_WORDS} (English, Portuguese, Spanish: Postgres' Snowball lists). `false`\n * keeps every word. With a language `config` (`english`) Postgres drops that language's stop\n * words as well.\n */\n stopWords?: Readonly<Record<string, ReadonlySet<string>>> | false;\n}\n\nexport interface PgVectorStoreOptions {\n /** Table name. Default `agent_rag_chunks`. */\n table?: string;\n /**\n * Embedding width — must match your model (e.g. 1536 for text-embedding-3-small). Default 1536.\n *\n * Pass a **list** to let several models' vectors share one table (mixed dimensions, e.g. while\n * migrating from a 768-wide local model to a 1536-wide hosted one): the column becomes an untyped\n * `vector`, `ensureSchema` creates one *partial* HNSW index per listed width\n * (`((embedding::vector(n))) … WHERE vector_dims(embedding) = n`), and `search` only compares the\n * query against vectors of its own width, through that width's index. A query whose width is not\n * listed is still answered — exactly, by a sequential scan. Filter on the model as well (put it in\n * the chunk metadata) when two models share a width.\n */\n dimensions?: number | readonly number[];\n /**\n * Allow chunks with no embedding (`embedding: []` on upsert is stored as `NULL`): index text first\n * and embed later, or keep full-text-only chunks when there is no embedding model at all. The\n * vector leg skips them; the lexical leg of {@link PgLexicalVectorStore} still finds them. Default\n * `false` — the column stays `NOT NULL`. Only affects the DDL `ensureSchema` emits for a new table.\n */\n nullableEmbeddings?: boolean;\n /**\n * pgvector ≥ 0.8 iterative index scans for `search`: keep walking the HNSW graph until enough rows\n * pass the metadata filter, instead of returning fewer than `topK` when a selective filter (one\n * tenant of many) rejects most of the candidates the index produced. Applied with `SET LOCAL`, so\n * it needs a client with a `transaction` (a `pg` Pool, Drizzle or postgres.js all have one); it is\n * silently skipped on an older pgvector or a client without transactions. Default off.\n */\n iterativeScan?: 'strict_order' | 'relaxed_order';\n /** `hnsw.ef_search` for `search` (candidate list size; pgvector's default is 40). Needs a `transaction`. */\n efSearch?: number;\n /**\n * Rows per multi-row `INSERT … ON CONFLICT` statement in `upsert`. Default 100. Duplicate ids in\n * one `upsert` call collapse to the last occurrence (as the old row-by-row loop left them).\n */\n upsertBatchSize?: number;\n}\n\n/** The one byte Postgres `text`/`jsonb` columns refuse outright. */\nconst NUL_BYTE = String.fromCharCode(0);\n\n/**\n * Strip the NUL byte (U+0000) out of a value before it reaches Postgres. `text` and `jsonb` columns\n * reject it outright — `invalid byte sequence for encoding \"UTF8\": 0x00` — and text extracted from\n * PDFs occasionally carries one even though other stores (Qdrant) accept it happily, so a chunk that\n * ingested fine elsewhere can fail here. Strings have the byte removed; arrays and **plain** objects\n * are walked recursively (object keys included, since a NUL can land in a metadata key too).\n *\n * \"Plain\" is deliberately narrow — `Object.getPrototypeOf(value)` must be `Object.prototype` or\n * `null` — so a `Date`, a `Buffer`, a class instance or any other non-plain object passes through\n * untouched rather than collapsing to `{}` via a blind `Object.entries`. Rebuilt objects go through\n * `Object.fromEntries` rather than assignment into a fresh `{}`, so a metadata key literally named\n * `__proto__` becomes an ordinary own property instead of silently repointing the result's prototype.\n */\nexport function stripNulBytes<T>(value: T): T {\n if (typeof value === 'string') {\n return (value.includes(NUL_BYTE) ? value.split(NUL_BYTE).join('') : value) as unknown as T;\n }\n if (Array.isArray(value)) {\n return value.map((item) => stripNulBytes(item)) as unknown as T;\n }\n if (value !== null && typeof value === 'object') {\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n return value;\n }\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, entry]) => [\n stripNulBytes(key),\n stripNulBytes(entry),\n ]),\n ) as unknown as T;\n }\n return value;\n}\n\n/**\n * A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the\n * `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call\n * {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index — or copy\n * {@link PgVectorStore.schemaStatements} into a migration.\n *\n * The constructor takes a `pg` `Pool`, a Drizzle database, a `postgres.js` `sql`, or a hand-written\n * {@link PgClient} (see {@link toPgClient}).\n *\n * This class does **not** implement the optional\n * {@link import('./vector-store.js').LexicalVectorStore} capability; {@link PgLexicalVectorStore}\n * (below) does. The split is deliberate: lexical search over Postgres needs a GIN index, and a\n * store that silently gained `searchText` on upgrade would be picked up by every\n * `isLexicalVectorStore(store)` check and start sequentially scanning an unindexed chunks table:\n *\n * - it is new DDL, and `CREATE INDEX` (non-concurrently, as `ensureSchema` must, since\n * `CONCURRENTLY` cannot run in a transaction) takes a write lock on an already-populated chunks\n * table — a boot-time stall proportional to corpus size for every existing deployment;\n * - it forces a text-search-configuration choice (`english` vs `simple` vs …) that must match between\n * the index expression and every query, or Postgres silently ignores the index and sequentially\n * scans the corpus — a failure mode that stays *correct* while quietly getting slower with scale.\n *\n * So lexical search is something you opt into by constructing the subclass, with the index created\n * by a migration you run and watch (or by `ensureSchema`, on a table small enough not to care).\n *\n * The enumeration and bulk-deletion methods {@link VectorStore} requires need nothing new here,\n * unlike the lexical capability above: enumeration and bulk deletion are `DISTINCT`, `= ANY($1)`, `DELETE … WHERE`\n * and `count(*)` over the table and index that already exist. No DDL, no boot-time lock, no migration.\n *\n * Subclassing: every statement filters through {@link PgVectorStore.whereConditions}, so a subclass\n * can map an opaque filter onto its own columns (a tenant id, an ACL) in one place.\n */\nexport class PgVectorStore implements VectorStore {\n protected readonly client: PgClient;\n protected readonly table: string;\n /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */\n private readonly dimensions: number | undefined;\n /** The widths with a partial HNSW index, for a mixed-dimension table. */\n private readonly indexedDimensions: readonly number[];\n private readonly nullableEmbeddings: boolean;\n private readonly iterativeScan: PgVectorStoreOptions['iterativeScan'];\n private readonly efSearch: number | undefined;\n private readonly upsertBatchSize: number;\n private iterativeScanSupport: Promise<boolean> | undefined;\n\n constructor(client: PgClientSource, options: PgVectorStoreOptions = {}) {\n this.client = toPgClient(client);\n this.table = options.table ?? 'agent_rag_chunks';\n const dimensions = options.dimensions ?? 1536;\n if (typeof dimensions === 'number') {\n this.dimensions = dimensions;\n this.indexedDimensions = [];\n } else {\n for (const width of dimensions) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);\n }\n }\n this.dimensions = undefined;\n this.indexedDimensions = [...new Set(dimensions)];\n }\n this.nullableEmbeddings = options.nullableEmbeddings ?? false;\n this.iterativeScan = options.iterativeScan;\n this.efSearch = options.efSearch;\n this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));\n }\n\n /** Telemetry self-description — the table is the namespace a retrieval was served from. */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'pg', collection: this.table };\n }\n\n /**\n * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead\n * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.\n */\n schemaStatements(): string[] {\n const mixed = this.dimensions === undefined;\n const column = mixed ? 'vector' : `vector(${this.dimensions})`;\n const statements = [\n 'CREATE EXTENSION IF NOT EXISTS vector',\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n source TEXT,\n metadata JSONB,\n embedding ${column}${this.nullableEmbeddings ? '' : ' NOT NULL'}\n )`,\n ];\n if (mixed) {\n for (const width of this.indexedDimensions) {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx\n ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)\n WHERE vector_dims(embedding) = ${width}`,\n );\n }\n } else {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx\n ON ${this.table} USING hnsw (embedding vector_cosine_ops)`,\n );\n }\n return statements;\n }\n\n /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */\n async ensureSchema(): Promise<void> {\n for (const statement of this.schemaStatements()) {\n await this.client.query(statement);\n }\n }\n\n /**\n * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}\n * rows per statement. An empty `embedding` is written as `NULL` (see\n * {@link PgVectorStoreOptions.nullableEmbeddings}).\n */\n async upsert(records: VectorRecord[]): Promise<void> {\n // Postgres rejects the NUL byte in text/jsonb — see stripNulBytes. The id is stripped before\n // de-duplication, so two ids that only differ by a NUL byte are the same row, as they'd be stored.\n const byId = new Map<string, VectorRecord>();\n for (const record of records) {\n const id = stripNulBytes(record.id);\n // Delete first so the surviving record takes the position of its LAST occurrence.\n byId.delete(id);\n byId.set(id, { ...record, id });\n }\n // One statement can't touch a row twice (`ON CONFLICT DO UPDATE command cannot affect row a\n // second time`), hence the de-duplication above: last write wins, as with the row-by-row loop.\n const unique = [...byId.values()];\n for (let start = 0; start < unique.length; start += this.upsertBatchSize) {\n const batch = unique.slice(start, start + this.upsertBatchSize);\n const params: unknown[] = [];\n const rows = batch.map((record) => {\n const text = stripNulBytes(record.text);\n const source = record.source !== undefined ? stripNulBytes(record.source) : undefined;\n const metadata = record.metadata !== undefined ? stripNulBytes(record.metadata) : undefined;\n const base = params.length;\n params.push(\n record.id,\n text,\n source ?? null,\n metadata !== undefined ? JSON.stringify(metadata) : null,\n record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null,\n );\n return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;\n });\n await this.client.query(\n `INSERT INTO ${this.table} (id, text, source, metadata, embedding)\n VALUES ${rows.join(', ')}\n ON CONFLICT (id) DO UPDATE SET\n text = EXCLUDED.text,\n source = EXCLUDED.source,\n metadata = EXCLUDED.metadata,\n embedding = EXCLUDED.embedding`,\n params,\n );\n }\n }\n\n async remove(documentId: string): Promise<void> {\n // Stripped so it matches the id `upsert` actually stored, and so a NUL-bearing id can't 0x00\n // the DELETE itself.\n await this.client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [\n stripNulBytes(documentId),\n ]);\n }\n\n /**\n * Rewrite a document's metadata without re-embedding it. See {@link VectorStore.updateMetadata}\n * for the semantics; here the whole thing is one statement, because jsonb already *is* the merge:\n * `||` is a shallow key-wise merge (right side wins, arrays replaced wholesale — precisely\n * {@link MetadataPatch}) and `- text[]` drops the keys the patch removed. `COALESCE` covers a chunk\n * ingested with no metadata at all, so a patch can create the object rather than no-op on `NULL`.\n *\n * Unlike RediSearch there is no second representation to keep in step — the `metadata` column is\n * both what `search` filters on and what it returns — so the correctness trap the Redis adapter has\n * to work for simply does not exist here. `RETURNING id` is what supplies the chunk count, since\n * {@link PgClient} is a rows-only surface with no `rowCount`.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n // Postgres rejects the NUL byte in text/jsonb — see stripNulBytes. `documentId` too, so it\n // matches the id `upsert` actually stored.\n const { set, remove } = splitMetadataPatch(stripNulBytes(patch));\n const rows = await this.client.query<{ id: string }>(\n `UPDATE ${this.table}\n SET metadata = (COALESCE(metadata, '{}'::jsonb) || $2::jsonb) - $3::text[]\n WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1\n RETURNING id`,\n [stripNulBytes(documentId), JSON.stringify(set), remove],\n );\n return rows.length;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const params: unknown[] = [];\n const where = this.where(filter, params);\n // DISTINCT ON collapses chunks to one row per document; all chunks share the doc's metadata.\n const rows = await this.client.query<{\n doc_id: string;\n metadata: Record<string, unknown> | null;\n }>(\n `SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata\n FROM ${this.table}\n ${where}\n ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,\n params,\n );\n return rows.map((row) => ({\n id: row.doc_id,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain\n * `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the\n * trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which\n * carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two\n * adapters.\n *\n * `embedding` is left out of the projection: it is the widest column in the table and useless to a\n * caller reading text back.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n // Stripped so it matches the id `upsert` actually stored.\n const params: unknown[] = [stripNulBytes(documentId)];\n let sql = `SELECT id, ${CHUNK_INDEX_FROM_ID} AS chunk_index, text, metadata\n FROM ${this.table}\n WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1\n ORDER BY chunk_index`;\n if (options?.limit !== undefined) {\n params.push(options.limit);\n sql += ` LIMIT $${params.length}`;\n }\n if (options?.offset !== undefined) {\n params.push(options.offset);\n sql += ` OFFSET $${params.length}`;\n }\n const rows = await this.client.query<{\n id: string;\n chunk_index: number;\n text: string;\n metadata: Record<string, unknown> | null;\n }>(sql, params);\n return rows.map((row) => ({\n id: row.id,\n index: Number(row.chunk_index),\n text: row.text,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n /** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ doc_id: string }>(\n `SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id\n FROM ${this.table}\n ${where}\n ORDER BY doc_id`,\n params,\n );\n return rows.map((row) => row.doc_id);\n }\n\n /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n // Stripped so every id matches what `upsert` actually stored.\n await this.client.query(\n `DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = ANY($1::text[])`,\n [stripNulBytes(documentIds)],\n );\n }\n\n /**\n * See {@link VectorStore.removeWhere}: empty filter object throws, empty-array value\n * deletes nothing. The count comes from `RETURNING id` rather than a driver-specific `rowCount`,\n * because {@link PgClient} is deliberately just \"run SQL, get rows\".\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n if (Object.keys(filter).length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the table. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n // Redundant with buildWhere's `false` clause, and stated anyway — the deny primitive should be\n // visible at the destructive call site, not one indirection away.\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ id: string }>(\n `DELETE FROM ${this.table} ${where} RETURNING id`,\n params,\n );\n return rows.length;\n }\n\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const params: unknown[] = [];\n const where = this.where(filter, params);\n const rows = await this.client.query<{ count: number | string }>(\n `SELECT count(*)::int AS count FROM ${this.table} ${where}`,\n params,\n );\n return Number(rows[0]?.count ?? 0);\n }\n\n /**\n * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query\n * are compared (through that width's partial index when it has one); chunks without an embedding\n * are never returned.\n */\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (embedding.length === 0) {\n return [];\n }\n // $1 = query vector, $2 = topK; metadata-filter params start at $3.\n const params: unknown[] = [toVectorLiteral(embedding), options.topK];\n const conditions = this.whereConditions(options.filter, params);\n let vectorType = 'vector';\n if (this.dimensions === undefined) {\n const width = embedding.length;\n if (this.indexedDimensions.includes(width)) {\n vectorType = `vector(${width})`;\n }\n conditions.push('embedding IS NOT NULL', `vector_dims(embedding) = ${width}`);\n } else if (this.nullableEmbeddings) {\n conditions.push('embedding IS NOT NULL');\n }\n // A fixed-width table keeps the exact expression its index was built on (`embedding`); a\n // mixed one casts both sides so the planner can match the per-width partial index.\n const column = vectorType === 'vector' ? 'embedding' : `(embedding::${vectorType})`;\n const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score\n FROM ${this.table}\n ${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}\n ORDER BY ${column} <=> $1::${vectorType}\n LIMIT $2`;\n const rows = await this.withSearchSettings((client) => client.query<PgRow>(sql, params));\n return rows.map(toPassage);\n }\n\n /**\n * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`\n * (placeholders are numbered after what `params` already holds). Every statement that takes a\n * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,\n * an access list — overrides this one method. Return `['false']` to deny.\n */\n protected whereConditions(\n filter: Record<string, unknown> | undefined,\n params: unknown[],\n ): string[] {\n return buildWhere(filter, params);\n }\n\n private where(filter: Record<string, unknown> | undefined, params: unknown[]): string {\n const conditions = this.whereConditions(filter, params);\n return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n }\n\n /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */\n private async withSearchSettings<T>(read: (client: PgClient) => Promise<T>): Promise<T> {\n const transaction = this.client.transaction?.bind(this.client);\n if (transaction === undefined) {\n return read(this.client);\n }\n const iterative = this.iterativeScan !== undefined && (await this.supportsIterativeScan());\n if (!iterative && this.efSearch === undefined) {\n return read(this.client);\n }\n return transaction(async (tx) => {\n if (iterative) {\n await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [this.iterativeScan]);\n }\n if (this.efSearch !== undefined) {\n await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [String(this.efSearch)]);\n }\n return read(tx);\n });\n }\n\n /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */\n private supportsIterativeScan(): Promise<boolean> {\n this.iterativeScanSupport ??= this.client\n .query<{ extversion: string }>(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`)\n .then((rows) => {\n const [major = 0, minor = 0] = (rows[0]?.extversion ?? '0.0').split('.').map(Number);\n return major > 0 || minor >= 8;\n })\n .catch(() => false);\n return this.iterativeScanSupport;\n }\n}\n\n/**\n * {@link PgVectorStore} plus Postgres full-text search: a {@link LexicalVectorStore}, so\n * `LexicalRetriever` and `HybridRetriever` work on Postgres exactly as on RediSearch — the dense leg\n * over pgvector, the lexical leg over a `tsvector`, both on the same rows and ids.\n *\n * A question is searched by its meaningful terms: its stop words dropped, rows holding any remaining\n * term ranked by IDF-weighted coverage, `ts_rank_cd` (cover density) breaking ties (see\n * {@link PgFullTextOptions.anyTermFallback}); explicit syntax (quoted phrases, `-exclusions`) goes\n * through `websearch_to_tsquery` first.\n *\n * The `tsvector` is either a column you own ({@link PgFullTextOptions.column}) or the expression\n * `to_tsvector(config, text)`, for which {@link PgVectorStore.schemaStatements} adds a GIN\n * expression index. See {@link PgVectorStore} for why that index belongs in a migration on a\n * populated table.\n */\nexport class PgLexicalVectorStore extends PgVectorStore implements LexicalVectorStore {\n private readonly tsConfig: string;\n private readonly tsColumn: string | undefined;\n private readonly anyTermFallback: boolean;\n private readonly stopWords: Readonly<Record<string, ReadonlySet<string>>>;\n\n constructor(\n client: PgClientSource,\n options: PgVectorStoreOptions & { fullText?: PgFullTextOptions } = {},\n ) {\n super(client, options);\n const config = options.fullText?.config ?? 'simple';\n if (!SQL_IDENTIFIER.test(config)) {\n throw new Error(`PgLexicalVectorStore: invalid text search configuration \"${config}\"`);\n }\n const column = options.fullText?.column;\n if (column !== undefined && !SQL_IDENTIFIER.test(column)) {\n throw new Error(`PgLexicalVectorStore: invalid tsvector column \"${column}\"`);\n }\n this.tsConfig = config;\n this.tsColumn = column;\n this.anyTermFallback = options.fullText?.anyTermFallback ?? true;\n const stopWords = options.fullText?.stopWords;\n this.stopWords = stopWords === false ? {} : (stopWords ?? DEFAULT_STOP_WORDS);\n }\n\n override schemaStatements(): string[] {\n const statements = super.schemaStatements();\n if (this.tsColumn === undefined) {\n statements.push(\n `CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx\n ON ${this.table} USING gin (${this.tsvector()})`,\n );\n }\n return statements;\n }\n\n async searchText(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter) || query.trim() === '') {\n return [];\n }\n if (!this.anyTermFallback || hasSearchSyntax(query)) {\n const strict = await this.runTextSearch(query, options);\n if (strict.length > 0 || !this.anyTermFallback) {\n return strict;\n }\n }\n const terms = keywordTerms(query, 24, this.stopWords);\n return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);\n }\n\n /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */\n private async runTextSearch(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n const tsquery = `websearch_to_tsquery('${this.tsConfig}', $1)`;\n // $1 = query, $2 = topK; metadata-filter params start at $3.\n const params: unknown[] = [query, options.topK];\n const conditions = [\n `${this.tsvector()} @@ ${tsquery}`,\n ...this.whereConditions(options.filter, params),\n ];\n const rows = await this.client.query<PgRow>(\n `SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score\n FROM ${this.table}\n WHERE ${conditions.join(' AND ')}\n ORDER BY score DESC, id\n LIMIT $2`,\n params,\n );\n return rows.map(toPassage);\n }\n\n /**\n * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,\n * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated\n * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full\n * of common words can match most of the table.\n */\n private async runRankedTermSearch(\n terms: string[],\n options: VectorSearchOptions,\n ): Promise<Passage[]> {\n const any = `to_tsquery('${this.tsConfig}', $1)`;\n // $1 = any-term tsquery, $2 = topK, $3 = terms; metadata-filter params start at $4.\n // Terms are letters/digits only, so quoting each one is enough to make it a literal lexeme.\n const params: unknown[] = [anyTermTsquery(terms), options.topK, terms];\n const conditions = [\n `${this.tsvector()} @@ ${any}`,\n ...this.whereConditions(options.filter, params),\n ];\n const rows = await this.client.query<PgRow>(\n `WITH __terms AS (\n SELECT DISTINCT to_tsquery('${this.tsConfig}', quote_literal(t)) AS q\n FROM unnest($3::text[]) AS t\n ), __cand AS MATERIALIZED (\n SELECT id, text, source, metadata, ${this.tsvector()} AS __tsv\n FROM ${this.table}\n WHERE ${conditions.join(' AND ')}\n ), __hits AS MATERIALIZED (\n SELECT __cand.id AS __id, __terms.q FROM __cand JOIN __terms ON __cand.__tsv @@ __terms.q\n ), __idf AS (\n SELECT q, ln(1 + ((SELECT count(*) FROM __cand) - count(*) + 0.5) / (count(*) + 0.5)) AS w\n FROM __hits GROUP BY q\n ), __scored AS (\n SELECT __hits.__id, sum(__idf.w) AS s, rank() OVER (ORDER BY sum(__idf.w) DESC) AS r\n FROM __hits JOIN __idf USING (q) GROUP BY __hits.__id\n )\n SELECT __cand.id, __cand.text, __cand.source, __cand.metadata,\n __scored.s + ts_rank_cd(__cand.__tsv, ${any}, 32) AS score\n FROM __scored JOIN __cand ON __cand.id = __scored.__id\n WHERE __scored.r <= $2\n ORDER BY __scored.s DESC, ts_rank_cd(__cand.__tsv, ${any}, 32) DESC, __cand.id\n LIMIT $2`,\n params,\n );\n return rows.map(toPassage);\n }\n\n /** Must be character-for-character the index expression, or the planner won't use the index. */\n private tsvector(): string {\n return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;\n }\n}\n\nconst SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/;\n\ninterface PgRow {\n id: string;\n text: string;\n source: string | null;\n metadata: Record<string, unknown> | null;\n score: number | string;\n}\n\nfunction toPassage(row: PgRow): Passage {\n return {\n id: row.id,\n text: row.text,\n score: Number(row.score),\n ...(row.source !== null ? { source: row.source } : {}),\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n };\n}\n\n/**\n * Build the conditions for a metadata `filter`, appending their bindings to `params` (placeholders\n * continue from `params.length`). Scalar values collapse into a single `@>` jsonb-containment check\n * (exact-match, as before). An **array** value is a **match-any** (OR / set membership) check via\n * jsonb `?|`: the record matches when its value for that key — scalar or array — shares an element\n * with the filter array. An empty array can never match (deny primitive). Keys are passed as\n * parameters (`metadata->$k`) so a caller-supplied metadata key can't inject SQL. Returns `[]` when\n * there is no filter, preserving the previous unfiltered query shape.\n *\n * The filter is run through {@link stripNulBytes} up front: both the keys (bound as `$k` params) and\n * the scalar/array values end up as query bindings, and a caller-supplied filter is exactly as\n * capable of carrying a NUL byte as the text it is filtering.\n */\nexport function buildWhere(\n filter: Record<string, unknown> | undefined,\n params: unknown[],\n): string[] {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return [];\n }\n const stripped = stripNulBytes(filter);\n const clauses: string[] = [];\n const scalar: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(stripped)) {\n if (Array.isArray(value)) {\n if (value.length === 0) {\n clauses.push('false');\n continue;\n }\n params.push(key);\n const keyParam = `$${params.length}`;\n params.push(value.map(String));\n const arrParam = `$${params.length}`;\n clauses.push(\n `(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' ` +\n `THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`,\n );\n } else {\n scalar[key] = value;\n }\n }\n if (Object.keys(scalar).length > 0) {\n params.push(JSON.stringify(scalar));\n clauses.push(`metadata @> $${params.length}::jsonb`);\n }\n return clauses;\n}\n\n/**\n * SQL that collapses a chunk id (`${documentId}#<n>`) back to its source document id — the pgvector\n * mirror of {@link import('./vector-store.js').documentIdOf}. Shared by `remove` and\n * `listDocuments` so both key on the exact same definition of \"chunk belongs to document\".\n */\nconst DOCUMENT_ID_FROM_CHUNK = \"regexp_replace(id, '#[0-9]+$', '')\";\n\n/** The `n` of `…#<n>` as an integer, so chunks sort numerically; a bare id (no suffix) is `0`. */\nconst CHUNK_INDEX_FROM_ID = \"COALESCE((substring(id from '#([0-9]+)$'))::int, 0)\";\n\n/** pgvector accepts a `'[1,2,3]'` text literal cast to `vector`. */\nfunction toVectorLiteral(embedding: number[]): string {\n return `[${embedding.join(',')}]`;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { filterMatchesNothing } from './filter.js';\nimport { type MetadataPatch, applyMetadataPatch, isEmptyMetadataPatch } from './metadata-patch.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport {\n type IndexedDocument,\n type LexicalVectorStore,\n type ListChunksOptions,\n type StoredChunk,\n UnsafeRemovalError,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n chunkIndexOf,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * The minimal Redis surface {@link RedisVectorStore} needs: send a raw command and get the reply.\n * Both `node-redis` (`client.sendCommand(args)`) and `ioredis` (wrap `redis.call`) satisfy it — so\n * this package pulls in NO Redis driver (bring your own, like the other adapters). Requires the\n * RediSearch module (Redis Stack / Redis 8+).\n */\nexport interface RedisSearchClient {\n sendCommand(args: (string | Buffer)[]): Promise<unknown>;\n}\n\n/**\n * The index that already exists in Redis contradicts this store's configuration in a way that cannot\n * be repaired in place. Thrown by {@link RedisVectorStore.ensureSchema} — which would otherwise have\n * returned happily and left every subsequent query quietly wrong.\n *\n * The canonical case is a changed embedding width (a model swap, 1024 → 1536): RediSearch fixes a\n * vector field's `DIM` at `FT.CREATE`, and every vector already stored was produced by the OLD model,\n * so there is no in-place fix — the index has to be dropped and the corpus re-embedded. That is\n * destructive and belongs to the host, never to a boot-time `ensureSchema`.\n */\nexport class RedisVectorSchemaMismatchError extends Error {\n constructor(\n /** The RediSearch index that was inspected. */\n readonly index: string,\n /** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */\n readonly field: string,\n /** What this store is configured for. */\n readonly expected: string,\n /** What `FT.INFO` reports the live index has. */\n readonly actual: string,\n remedy: string,\n ) {\n super(\n `RediSearch index \"${index}\" disagrees with this RedisVectorStore's configuration: ` +\n `field \"${field}\" is ${actual}, expected ${expected}. ${remedy}`,\n );\n this.name = 'RedisVectorSchemaMismatchError';\n }\n}\n\nexport interface RedisVectorStoreOptions {\n /** RediSearch index name. Default `agent_rag_idx`. */\n index?: string;\n /** Key prefix for the per-chunk hashes. Default `agent_rag:`. */\n prefix?: string;\n /** Embedding width — must match your model (e.g. 1536). Default 1536. */\n dimensions?: number;\n /**\n * Metadata keys to index as filterable TAG fields. RediSearch needs filter fields declared up\n * front, so only keys listed here can be passed in `search`'s `filter`. Default none.\n */\n filterableFields?: string[];\n /**\n * RediSearch scorer used by {@link RedisVectorStore.searchText}. Default `BM25` — the standard\n * lexical ranking, and the same family the in-process `KeywordRetriever` implements. Override for\n * a server that names it differently (`BM25STD` on newer builds) or to pick `TFIDF`/`DISMAX`.\n */\n lexicalScorer?: string;\n}\n\n/**\n * A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option\n * for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the\n * embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}\n * once at boot to create the index.\n *\n * The chunk text is declared as a RediSearch `TEXT` field, so this store also satisfies\n * {@link LexicalVectorStore}: {@link RedisVectorStore.searchText} is BM25 over that same index —\n * the lexical half of hybrid search with no second index to build, feed, or invalidate.\n */\nexport class RedisVectorStore implements LexicalVectorStore {\n private readonly index: string;\n private readonly prefix: string;\n private readonly dimensions: number;\n private readonly filterableFields: string[];\n private readonly lexicalScorer: string;\n\n constructor(\n private readonly client: RedisSearchClient,\n options: RedisVectorStoreOptions = {},\n ) {\n this.index = options.index ?? 'agent_rag_idx';\n this.prefix = options.prefix ?? 'agent_rag:';\n this.dimensions = options.dimensions ?? 1536;\n this.filterableFields = options.filterableFields ?? [];\n this.lexicalScorer = options.lexicalScorer ?? 'BM25';\n }\n\n /**\n * Telemetry self-description — the RediSearch INDEX, not the key prefix: the index is what both\n * legs query, and two stores sharing one index (the dense and lexical halves of a hybrid) must\n * report the same namespace or `describeSharedSource` would see them disagree and drop the store.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { store: 'redis', collection: this.index };\n }\n\n /**\n * Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —\n * and if it DOES exist, compare what's there against this store's configuration instead of\n * assuming they agree. An index that outlives a config change is the normal case (schemas live in\n * Redis, config lives in a redeployed process), and the two failure modes it produces are both\n * invisible at runtime: a `filterableFields` entry added later has no `meta_*` TAG, so every search\n * filtering on it matches *nothing*; a changed `dimensions` leaves the index on the old width, so\n * writes are rejected or KNN ranks garbage. Neither raises anything on its own.\n *\n * The two are repaired differently, because only one of them CAN be repaired:\n *\n * - **A missing filterable TAG is additive** → `FT.ALTER … SCHEMA ADD`. Nothing that already works\n * changes, so doing it automatically is safe. Note the chunks written *before* the alter carry no\n * `meta_*` hash field at all (`upsert` only writes the fields configured at the time), so they\n * only become filterable on that key once they're re-ingested — the schema is repaired here, the\n * backfill is the host's call.\n * - **A dimension (or field-type) change is not repairable** → {@link RedisVectorSchemaMismatchError}.\n * It needs a drop + full reindex, which would destroy the host's corpus; failing loudly at boot is\n * the only honest option.\n *\n * If `FT.INFO` comes back in a shape this parser doesn't recognise, no drift is inferred — an\n * unreadable reply must not be turned into a false alarm that blocks a boot.\n */\n async ensureSchema(): Promise<void> {\n let info: unknown;\n try {\n info = await this.client.sendCommand(['FT.INFO', this.index]);\n } catch {\n await this.createIndex();\n return;\n }\n await this.reconcileSchema(info);\n }\n\n /** Repair what can be repaired in place; throw on what can't. */\n private async reconcileSchema(info: unknown): Promise<void> {\n const attributes = parseIndexAttributes(info);\n if (attributes.size === 0) {\n return;\n }\n\n const vector = attributes.get('embedding');\n if (vector?.dimensions !== undefined && vector.dimensions !== this.dimensions) {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n 'embedding',\n `DIM ${this.dimensions}`,\n `DIM ${vector.dimensions}`,\n `A vector field's width is fixed at FT.CREATE and every stored embedding has the old one, so this cannot be altered in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest with the new embedding model.`,\n );\n }\n\n const missing: string[] = [];\n for (const field of this.filterableFields) {\n const name = `meta_${field}`;\n const existing = attributes.get(name);\n if (existing === undefined) {\n missing.push(name);\n } else if (existing.type !== 'TAG') {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n name,\n 'TAG',\n existing.type,\n `A field's type cannot be changed in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest, or rename the metadata key.`,\n );\n }\n }\n for (const name of missing) {\n await this.client.sendCommand(['FT.ALTER', this.index, 'SCHEMA', 'ADD', name, 'TAG']);\n }\n }\n\n private async createIndex(): Promise<void> {\n const schema: string[] = ['text', 'TEXT', 'source', 'TAG', 'metadata_json', 'TEXT', 'NOINDEX'];\n for (const field of this.filterableFields) {\n schema.push(`meta_${field}`, 'TAG');\n }\n schema.push(\n 'embedding',\n 'VECTOR',\n 'HNSW',\n '6',\n 'TYPE',\n 'FLOAT32',\n 'DIM',\n String(this.dimensions),\n 'DISTANCE_METRIC',\n 'COSINE',\n );\n await this.client.sendCommand([\n 'FT.CREATE',\n this.index,\n 'ON',\n 'HASH',\n 'PREFIX',\n '1',\n this.prefix,\n 'SCHEMA',\n ...schema,\n ]);\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n const args: (string | Buffer)[] = [\n 'HSET',\n `${this.prefix}${record.id}`,\n 'text',\n record.text,\n 'embedding',\n encodeVector(record.embedding),\n ];\n if (record.source !== undefined) {\n args.push('source', record.source);\n }\n if (record.metadata !== undefined) {\n args.push('metadata_json', JSON.stringify(record.metadata));\n for (const field of this.filterableFields) {\n const value = record.metadata[field];\n if (value !== undefined) {\n args.push(`meta_${field}`, encodeTag(value));\n }\n }\n }\n await this.client.sendCommand(args);\n }\n }\n\n /**\n * Single-document deletion, by `SCAN … MATCH`.\n *\n * This deliberately stays a keyspace scan even though the rest of this class no longer needs one.\n * The obvious optimisation is to stamp each chunk with its document id as an implicit filterable\n * TAG at {@link RedisVectorStore.upsert} and turn `remove` into an `FT.SEARCH` — and that is exactly\n * the change that must not ship, because chunks written *before* it carry no such field. `FT.SEARCH`\n * would find zero of them, `remove` would return happily, and a document that has been \"deleted\"\n * would go on being retrieved. A delete that silently stops deleting is worse than a slow one, and\n * the library cannot tell a fully-migrated corpus from an unmigrated one: both look like \"this\n * document has no indexed chunks\". Nor is a search-then-fall-back-to-scan hybrid safe — a document\n * re-ingested after the upgrade has *some* stamped chunks, so the search finds hits, the fallback\n * never runs, and the unstamped tail survives. That is the same silent failure with a smaller blast\n * radius, which makes it harder to notice rather than less wrong.\n *\n * The consumer pain this was measured against — deleting a whole collection, one `remove` per\n * document — is answered instead by {@link RedisVectorStore.removeWhere} (one filtered query, no\n * keyspace scan at all) and {@link RedisVectorStore.removeMany} (one scan for N documents rather\n * than N scans). Neither depends on a field that older chunks lack, so neither has a migration.\n */\n async remove(documentId: string): Promise<void> {\n const keys = await this.chunkKeys(documentId);\n if (keys.length > 0) {\n await this.client.sendCommand(['DEL', ...keys]);\n }\n }\n\n /**\n * Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`\n * alone. See {@link VectorStore.updateMetadata} for the merge semantics and the return value.\n *\n * The care this needs is entirely about the fact that a chunk stores its metadata **twice**: as the\n * `metadata_json` blob (what {@link RedisVectorStore.search} reads back onto a `Passage`) and as\n * `meta_<field>` TAG fields (what RediSearch actually filters on, and only for the declared\n * `filterableFields`). Move one without the other and the store lies to itself — a chunk that\n * filters as the old value but reports the new one, or the reverse — so this method rewrites both\n * from the same merged object:\n *\n * - a patched key that is **not** filterable lands in `metadata_json` only, and no stray `meta_*`\n * field is invented for it (it would be dead weight the index does not know about);\n * - a patched key that **is** filterable also rewrites its TAG, using the identical encoding\n * `upsert` uses, so an updated chunk is indistinguishable from a re-ingested one;\n * - a filterable key the patch **removes** has its TAG `HDEL`-ed, not left behind as a tag matching\n * a value the document no longer carries.\n *\n * A plain `HSET` is all the index needs: RediSearch re-indexes a hash the moment it is written, so\n * the very next `FT.SEARCH` sees the new TAG and no longer the old one — no reindex command, and\n * no need to rewrite the (untouched, and expensive) vector to nudge it. `HDEL` on an indexed field\n * likewise drops it from the index. Verified against a real Redis Stack in\n * `redis-vector-store.db.spec.ts`, not assumed.\n *\n * Removals are issued **before** the write, so the only window a concurrent reader can observe is\n * \"TAG already gone, `metadata_json` not yet updated\" — which fails *closed* on a filter carrying\n * an ACL. The reverse order would briefly leave a document tagged with a capability its metadata no\n * longer claims. Read-modify-write across chunks is not transactional; concurrent writers to the\n * same chunk are last-writer-wins, exactly as two concurrent `upsert`s already are.\n */\n async updateMetadata(documentId: string, patch: MetadataPatch): Promise<number> {\n if (isEmptyMetadataPatch(patch)) {\n return 0;\n }\n const keys = await this.chunkKeys(documentId);\n let updated = 0;\n for (const key of keys) {\n const raw = await this.client.sendCommand(['HGET', key, 'metadata_json']);\n const current = parseMetadata(raw === null || raw === undefined ? undefined : toStr(raw));\n const next = applyMetadataPatch(current.metadata, patch);\n\n const args: (string | Buffer)[] = ['HSET', key, 'metadata_json', JSON.stringify(next)];\n const stale: string[] = [];\n for (const field of this.filterableFields) {\n const value = next[field];\n if (value !== undefined) {\n args.push(`meta_${field}`, encodeTag(value));\n } else if (current.metadata?.[field] !== undefined) {\n stale.push(`meta_${field}`);\n }\n }\n if (stale.length > 0) {\n await this.client.sendCommand(['HDEL', key, ...stale]);\n }\n await this.client.sendCommand(args);\n updated += 1;\n }\n return updated;\n }\n\n /**\n * Every chunk key that currently EXISTS for `documentId` — the bare `${prefix}${documentId}` plus\n * the `${documentId}#<n>` chunks found by `SCAN`. One definition of \"chunk belongs to document\",\n * shared by `remove` and `updateMetadata`, so the two can never disagree about which keys a\n * document owns. The glob is escaped so `gone` does not reach into `goner#0`.\n */\n private async chunkKeys(documentId: string): Promise<string[]> {\n const keys = new Set<string>();\n const bare = `${this.prefix}${documentId}`;\n const exists = await this.client.sendCommand(['EXISTS', bare]);\n if (Number(toStr(exists)) > 0) {\n keys.add(bare);\n }\n const pattern = `${this.prefix}${escapeGlob(documentId)}#*`;\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n pattern,\n 'COUNT',\n '256',\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n keys.add(key);\n }\n } while (cursor !== '0');\n return [...keys];\n }\n\n /**\n * The chunks of one document, in document order. See {@link VectorStore.listChunks}.\n *\n * Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.\n * Two reasons, and the second is the one that matters: the query language has no way to ask for\n * \"every chunk of this document\" — `id` is the key name, not an indexed field — and `FT.SEARCH`\n * caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does\n * not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and\n * {@link RedisVectorStore.updateMetadata} already use, so \"the chunks this returns\" and \"the chunks\n * a delete would take\" cannot drift apart.\n *\n * It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this\n * is an inspection path (one document, on demand) and not something retrieval runs — but it is why\n * this must not be put on a hot path or called in a loop over a corpus.\n *\n * `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing\n * a caller reading text back can do with it.\n */\n async listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]> {\n const keys = await this.chunkKeys(documentId);\n const ordered = keys\n .map((key) => ({ key, id: key.slice(this.prefix.length) }))\n .map((entry) => ({ ...entry, index: chunkIndexOf(entry.id) }))\n .sort((a, b) => a.index - b.index);\n\n const offset = options?.offset ?? 0;\n const page =\n options?.limit === undefined\n ? ordered.slice(offset)\n : ordered.slice(offset, offset + options.limit);\n\n const chunks: StoredChunk[] = [];\n for (const entry of page) {\n const reply = await this.client.sendCommand(['HMGET', entry.key, 'text', 'metadata_json']);\n const fields = Array.isArray(reply) ? reply : [];\n const text = fields[0] === null || fields[0] === undefined ? undefined : toStr(fields[0]);\n if (text === undefined) {\n // The key was deleted between the SCAN and this read. Skip it rather than reporting a chunk\n // with empty text, which would read as \"the chunker produced nothing here\".\n continue;\n }\n const raw = fields[1] === null || fields[1] === undefined ? undefined : toStr(fields[1]);\n chunks.push({\n id: entry.id,\n index: entry.index,\n text,\n ...parseMetadata(raw),\n });\n }\n return chunks;\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Map<string, IndexedDocument>();\n const batchSize = 1000;\n let offset = 0;\n for (;;) {\n // RETURN metadata_json (not NOCONTENT) so we can carry a representative chunk's metadata;\n // parseSearchReply already yields prefix-stripped ids + parsed metadata for both RESP2/RESP3.\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'RETURN',\n '1',\n 'metadata_json',\n 'LIMIT',\n String(offset),\n String(batchSize),\n 'DIALECT',\n '2',\n ]);\n const passages = parseSearchReply(reply, this.prefix);\n for (const passage of passages) {\n const id = documentIdOf(passage.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(passage.metadata !== undefined ? { metadata: passage.metadata } : {}),\n });\n }\n }\n if (passages.length < batchSize) {\n break;\n }\n offset += batchSize;\n }\n return [...documents.values()];\n }\n\n /**\n * The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:\n * RediSearch returns bare keys, so nothing carries `metadata_json` over the wire and nothing is\n * JSON-parsed. `listDocuments` has to parse one metadata blob per *chunk* — thousands of parses to\n * produce a few hundred ids it then throws the metadata away from — and this does none of it.\n *\n * The collapse to document ids still happens client-side. RediSearch can group server-side\n * (`FT.AGGREGATE … GROUPBY`), but only over an indexed *field*, and a chunk's document id lives in\n * its key, not in a field — deriving it would mean stamping one at write time, which is the\n * migration {@link RedisVectorStore.remove} explains this class refuses. Deduplicating a page of\n * keys in JS is free by comparison.\n */\n async listDocumentIds(filter?: Record<string, unknown>): Promise<string[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Set<string>();\n let offset = 0;\n for (;;) {\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'NOCONTENT',\n 'LIMIT',\n String(offset),\n String(SCAN_PAGE),\n 'DIALECT',\n '2',\n ]);\n const keys = parseSearchKeys(reply);\n for (const key of keys) {\n documents.add(documentIdOf(stripPrefix(key, this.prefix)));\n }\n if (keys.length < SCAN_PAGE) {\n break;\n }\n offset += SCAN_PAGE;\n }\n return [...documents];\n }\n\n /**\n * Bulk deletion by document id — **one** keyspace scan for N documents, where N calls to\n * {@link RedisVectorStore.remove} would be N of them. The scan is `MATCH ${prefix}*`, so Redis still\n * filters to this store's own keys server-side; membership is then decided in JS against a `Set`,\n * which is what lets a single pass serve any number of ids.\n *\n * It deletes by key, exactly as `remove` does, so it is correct on chunks written by any version of\n * this library — there is no field it needs the corpus to already carry.\n */\n async removeMany(documentIds: string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n const wanted = new Set(documentIds);\n // A document may have been stored under its bare id (no `#n` suffix); those keys are exact and\n // need no scan to find, so seed them and let the scan add the chunked ones.\n let pending = documentIds.map((id) => `${this.prefix}${id}`);\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n `${escapeGlob(this.prefix)}*`,\n 'COUNT',\n String(SCAN_PAGE),\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n if (wanted.has(documentIdOf(stripPrefix(key, this.prefix)))) {\n pending.push(key);\n }\n }\n if (pending.length >= DELETE_BATCH) {\n await this.client.sendCommand(['DEL', ...pending]);\n pending = [];\n }\n } while (cursor !== '0');\n if (pending.length > 0) {\n await this.client.sendCommand(['DEL', ...pending]);\n }\n }\n\n /**\n * Delete every chunk matching `filter`, via `FT.SEARCH … NOCONTENT` + `DEL` — no keyspace scan at\n * all, so dropping a collection costs one indexed query per page instead of one full pass per\n * document. Resolves the number of chunk keys actually deleted.\n *\n * The two refusals, in this order, before any query is issued:\n *\n * 1. **An empty filter object throws** {@link UnsafeRemovalError}. `buildFilter({})` is `*`, and `*`\n * here would mean \"delete the corpus\" — far too destructive an outcome for the shape a filter\n * degenerates to when it is built wrong.\n * 2. **An empty-array value deletes nothing** and resolves `0`. This is the package's deny primitive\n * (an actor with no capability tokens), and it is the one that has to be gated *explicitly*: a\n * plausible-looking \"normalise the filter first\" refactor that drops empty arrays turns a deny\n * into `*` and wipes everything the deny existed to protect.\n *\n * Then one more, which is specific to RediSearch: every filter key must be a declared\n * `filterableFields` entry. `search` lets the engine reject an unknown field, which is fine for a\n * read; for a delete the check is worth making up front and by name.\n *\n * Deletion happens page by page, always re-querying from offset 0 — the rows just deleted are gone\n * from the index, so paging forward would skip their replacements. If a page comes back non-empty\n * but `DEL` reports nothing removed (keys expired between query and delete, or the index is stale),\n * the loop stops rather than spinning on results it can never consume.\n */\n async removeWhere(filter: Record<string, unknown>): Promise<number> {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n throw new UnsafeRemovalError(\n 'empty-filter',\n 'removeWhere() refuses an empty filter: it would delete every chunk in the index. ' +\n 'Pass a filter that scopes the removal, or delete deliberately with ' +\n 'removeMany(await store.listDocumentIds()).',\n );\n }\n const unindexed = keys.filter((key) => !this.filterableFields.includes(key));\n if (unindexed.length > 0) {\n const named = unindexed.map((key) => `\"${key}\"`).join(', ');\n const declared = this.filterableFields.join(', ') || 'none';\n throw new UnsafeRemovalError(\n 'unindexed-field',\n `removeWhere() cannot scope on ${named}: not declared in this store's filterableFields (${declared}), so RediSearch has no TAG to match on.`,\n );\n }\n // THE guard. Removing it turns the deny primitive into an unfiltered delete — see the db spec's\n // \"an empty-array filter deletes NOTHING\" case, which exists to fail loudly if this ever goes.\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n\n const query = buildFilter(filter);\n let removed = 0;\n for (;;) {\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'NOCONTENT',\n 'LIMIT',\n '0',\n String(DELETE_BATCH),\n 'DIALECT',\n '2',\n ]);\n const page = parseSearchKeys(reply);\n if (page.length === 0) {\n break;\n }\n const deleted = Number(await this.client.sendCommand(['DEL', ...page]));\n if (!Number.isFinite(deleted) || deleted === 0) {\n break;\n }\n removed += deleted;\n }\n return removed;\n }\n\n /**\n * Count matching chunks with `FT.SEARCH … LIMIT 0 0` — RediSearch returns the total and no\n * documents, so the cost is the query, not the corpus. An empty-array filter value denies and\n * yields `0`, same as every other filtered path here.\n */\n async countChunks(filter?: Record<string, unknown>): Promise<number> {\n if (filterMatchesNothing(filter)) {\n return 0;\n }\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n buildFilter(filter),\n 'NOCONTENT',\n 'LIMIT',\n '0',\n '0',\n 'DIALECT',\n '2',\n ]);\n return parseSearchTotal(reply);\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const query = `${buildFilter(options.filter)}=>[KNN ${options.topK} @embedding $BLOB AS vector_score]`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'PARAMS',\n '2',\n 'BLOB',\n encodeVector(embedding),\n 'RETURN',\n '4',\n 'text',\n 'source',\n 'metadata_json',\n 'vector_score',\n 'SORTBY',\n 'vector_score',\n 'ASC',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix);\n }\n\n /**\n * Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of\n * hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the\n * corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as\n * `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector\n * hit would have respected.\n *\n * A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is\n * deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a\n * relevance query with nothing to be relevant to should do.\n */\n async searchText(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const terms = tokenizeQuery(query);\n if (terms.length === 0) {\n return [];\n }\n const filterQuery = buildFilter(options.filter);\n const textQuery = `@text:(${terms.join('|')})`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n filterQuery === '*' ? textQuery : `(${filterQuery} ${textQuery})`,\n 'SCORER',\n this.lexicalScorer,\n 'WITHSCORES',\n 'RETURN',\n '3',\n 'text',\n 'source',\n 'metadata_json',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix, 'lexical');\n }\n}\n\n/** Page size for `SCAN COUNT` and for the id-only `FT.SEARCH` pages. */\nconst SCAN_PAGE = 1000;\n\n/** How many keys to hand a single `DEL`, and therefore how big a `removeWhere` page is. */\nconst DELETE_BATCH = 512;\n\n/** Recover a chunk id from its Redis key. */\nfunction stripPrefix(key: string, prefix: string): string {\n return key.startsWith(prefix) ? key.slice(prefix.length) : key;\n}\n\n/**\n * Parse the keys out of an `FT.SEARCH … NOCONTENT` reply, tolerating both wire shapes: the RESP2 array\n * `[total, key, key, …]` and the RESP3 object `{ total_results, results: [{ id }] }`. Keys come back\n * whole (prefix included) — callers strip it.\n */\nfunction parseSearchKeys(reply: unknown): string[] {\n if (Array.isArray(reply)) {\n return reply.slice(1).map(toStr);\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n return Array.isArray(results) ? results.map(readId) : [];\n }\n return [];\n}\n\n/**\n * Parse the match count out of an `FT.SEARCH … LIMIT 0 0` reply — element 0 in RESP2, `total_results`\n * in the RESP3 object. An unrecognised shape counts as 0 rather than `NaN`, so a caller comparing\n * against a threshold can't be handed something that fails every comparison silently.\n */\nfunction parseSearchTotal(reply: unknown): number {\n if (Array.isArray(reply)) {\n const total = Number(toStr(reply[0]));\n return Number.isFinite(total) ? total : 0;\n }\n if (typeof reply === 'object' && reply !== null && 'total_results' in reply) {\n const total = Number((reply as { total_results: unknown }).total_results);\n return Number.isFinite(total) ? total : 0;\n }\n return 0;\n}\n\n/** One attribute as the live index reports it — enough to spot the drift that matters. */\ninterface IndexAttribute {\n /** `TEXT` / `TAG` / `VECTOR`. */\n type: string;\n /** `VECTOR` fields only: the width the index was created with. */\n dimensions?: number;\n}\n\n/**\n * Parse `FT.INFO`'s `attributes` into `attribute name → { type, dimensions }`, tolerating both wire\n * shapes exactly like {@link parseSearchReply} does: the RESP2 flat key/value array (whose attributes\n * are themselves flat key/value arrays) and node-redis's RESP3 object (attributes as objects).\n * Unknown shapes yield an empty map, which callers read as \"can't tell\" rather than \"nothing there\".\n */\nfunction parseIndexAttributes(reply: unknown): Map<string, IndexAttribute> {\n const attributes = new Map<string, IndexAttribute>();\n for (const entry of readInfoField(reply, 'attributes')) {\n const fields = readKeyValues(entry);\n const name = fields.attribute ?? fields.identifier;\n if (name === undefined) {\n continue;\n }\n const dim = Number(fields.dim);\n attributes.set(name, {\n type: (fields.type ?? '').toUpperCase(),\n ...(fields.dim !== undefined && Number.isFinite(dim) ? { dimensions: dim } : {}),\n });\n }\n return attributes;\n}\n\n/** Read one top-level `FT.INFO` field as an array, from either the RESP2 pair-array or RESP3 object. */\nfunction readInfoField(reply: unknown, key: string): unknown[] {\n if (Array.isArray(reply)) {\n for (let index = 0; index + 1 < reply.length; index += 2) {\n if (toStr(reply[index]) === key) {\n const value = reply[index + 1];\n return Array.isArray(value) ? value : [];\n }\n }\n return [];\n }\n if (typeof reply === 'object' && reply !== null && key in reply) {\n const value = (reply as Record<string, unknown>)[key];\n return Array.isArray(value) ? value : [];\n }\n return [];\n}\n\n/** Flatten one attribute entry (RESP2 pair-array or RESP3 object) into lower-cased string fields. */\nfunction readKeyValues(entry: unknown): Record<string, string | undefined> {\n const fields: Record<string, string | undefined> = {};\n if (Array.isArray(entry)) {\n for (let index = 0; index + 1 < entry.length; index += 2) {\n fields[toStr(entry[index]).toLowerCase()] = toStr(entry[index + 1]);\n }\n return fields;\n }\n if (typeof entry === 'object' && entry !== null) {\n for (const [key, value] of Object.entries(entry)) {\n fields[key.toLowerCase()] = toStr(value);\n }\n }\n return fields;\n}\n\n/** RediSearch expects a little-endian FLOAT32 buffer for the query/stored vector. */\nfunction encodeVector(embedding: number[]): Buffer {\n return Buffer.from(new Float32Array(embedding).buffer);\n}\n\n/**\n * `*` for no filter, else an AND of TAG clauses. A scalar value is an exact tag match\n * (`@meta_owner:{u1}`); an **array** value is a TAG alternation — match-any / OR\n * (`@meta_audience:{public|role\\:ADMIN}`), the capability-ACL primitive. RediSearch TAG fields are\n * multi-valued per document, so this also matches documents carrying several of the tags.\n */\nfunction buildFilter(filter?: Record<string, unknown>): string {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return '*';\n }\n const clauses = Object.entries(filter).map(([key, value]) => {\n const values = Array.isArray(value) ? value : [value];\n const alternation = values.map((entry) => escapeTag(String(entry))).join('|');\n return `@meta_${key}:{${alternation}}`;\n });\n return `(${clauses.join(' ')})`;\n}\n\n/**\n * Reduce a raw user query to RediSearch query *terms*, the union of which becomes the text clause.\n *\n * This is the escaping story for the text leg, and it escapes by **construction** rather than by\n * quoting: a term is kept only as a run of letters, digits and `_`, so nothing that could carry\n * meaning in RediSearch's query language — `@ { } | ( ) * ~ - \" ' : ; , => %` and friends — can\n * survive into the emitted query. A query cannot therefore add a clause, negate one, open a field\n * selector, or widen the filter it is ANDed with. (Escaping each punctuation character instead, as\n * {@link escapeTag} does for TAG values, would be safe but *lossy*: RediSearch's own tokenizer split\n * the indexed text on that same punctuation, so an escaped `hello\\-world` term matches nothing.)\n *\n * Unicode letters and digits are kept — stripping to ASCII would silently make non-English corpora\n * unsearchable. Since `\\p{L}`/`\\p{N}` contain no RediSearch metacharacters, keeping them is safe.\n */\nfunction tokenizeQuery(query: string): string[] {\n return query.match(/[\\p{L}\\p{N}_]+/gu) ?? [];\n}\n\n/**\n * Encode one metadata value as the `meta_<field>` hash field RediSearch indexes as a TAG. An **array**\n * becomes a multi-valued TAG (comma-separated, RediSearch's default separator) so a document can\n * carry several capability tokens — which is also why individual values must not contain commas.\n *\n * Shared by `upsert` and `updateMetadata` on purpose: they write the same field, and the day the two\n * encode it differently is the day a patched chunk stops matching a filter a freshly-ingested one\n * matches, for a value that looks identical in `metadata_json`.\n */\nfunction encodeTag(value: unknown): string {\n return Array.isArray(value) ? value.map(String).join(',') : String(value);\n}\n\n/** Escape RediSearch TAG punctuation so an id/tenant value matches literally. */\nfunction escapeTag(value: string): string {\n return value.replace(/[^a-zA-Z0-9_]/g, (char) => `\\\\${char}`);\n}\n\n/** Escape `SCAN MATCH` glob metacharacters so a document id matches its chunk keys literally. */\nfunction escapeGlob(value: string): string {\n return value.replace(/[*?[\\]\\\\]/g, (char) => `\\\\${char}`);\n}\n\n/**\n * Parse a `SCAN` reply into `{ cursor, keys }`, tolerating both wire shapes: the RESP2 array\n * `[cursor, [key, …]]` and node-redis's RESP3 object `{ cursor, keys }`.\n */\nfunction parseScanReply(reply: unknown): { cursor: string; keys: string[] } {\n if (Array.isArray(reply) && reply.length >= 2) {\n const rawKeys = reply[1];\n return {\n cursor: toStr(reply[0]),\n keys: Array.isArray(rawKeys) ? rawKeys.map(toStr) : [],\n };\n }\n if (typeof reply === 'object' && reply !== null && 'cursor' in reply && 'keys' in reply) {\n const { cursor, keys } = reply as { cursor: unknown; keys: unknown };\n return {\n cursor: toStr(cursor),\n keys: Array.isArray(keys) ? keys.map(toStr) : [],\n };\n }\n return { cursor: '0', keys: [] };\n}\n\nfunction toStr(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);\n}\n\n/**\n * Which leg produced the reply, and therefore where a passage's `score` comes from:\n *\n * - `vector` — the KNN query. Score is the returned `vector_score` **distance**, turned into a\n * cosine similarity by `1 - distance`. Two array elements per result (`key`, `fields`).\n * - `lexical` — the `WITHSCORES` text query. The engine's BM25 relevance is already a\n * bigger-is-better score, and it arrives *inline*: three array elements per result\n * (`key`, `score`, `fields`), which is why the stride is not a constant.\n */\ntype ScoreSource = 'vector' | 'lexical';\n\n/**\n * Parse an `FT.SEARCH` reply into passages, tolerating both wire shapes: the RESP2 array\n * `[total, key, [f, v, …], …]` (ioredis / node-redis RESP2) and the RESP3 object\n * `{ results: [{ id, extra_attributes: { … } }] }` (node-redis's default). The `prefix` is stripped\n * back off the key to recover the chunk id; `scoreSource` selects the scoring convention.\n */\nfunction parseSearchReply(\n reply: unknown,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n): Passage[] {\n const stride = scoreSource === 'lexical' ? 3 : 2;\n if (Array.isArray(reply)) {\n const passages: Passage[] = [];\n for (let index = 1; index + stride - 1 < reply.length; index += stride) {\n const rawFields = reply[index + stride - 1];\n if (Array.isArray(rawFields)) {\n const attrs: Record<string, string> = {};\n for (let field = 0; field + 1 < rawFields.length; field += 2) {\n attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);\n }\n const inlineScore = scoreSource === 'lexical' ? toStr(reply[index + 1]) : undefined;\n passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));\n }\n }\n return passages;\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n if (Array.isArray(results)) {\n return results.map((result) =>\n toPassage(\n readId(result),\n readAttributes(result),\n prefix,\n scoreSource,\n scoreSource === 'lexical' ? readScore(result) : undefined,\n ),\n );\n }\n }\n return [];\n}\n\n/** RESP3 `WITHSCORES` puts the relevance score on the result object rather than inline in the array. */\nfunction readScore(result: unknown): string | undefined {\n if (typeof result === 'object' && result !== null && 'score' in result) {\n return toStr((result as { score: unknown }).score);\n }\n return undefined;\n}\n\nfunction readId(result: unknown): string {\n if (typeof result === 'object' && result !== null && 'id' in result) {\n return toStr((result as { id: unknown }).id);\n }\n return '';\n}\n\nfunction readAttributes(result: unknown): Record<string, string> {\n const attrs: Record<string, string> = {};\n if (typeof result === 'object' && result !== null && 'extra_attributes' in result) {\n const raw = (result as { extra_attributes: unknown }).extra_attributes;\n if (typeof raw === 'object' && raw !== null) {\n for (const [key, value] of Object.entries(raw)) {\n attrs[key] = toStr(value);\n }\n }\n }\n return attrs;\n}\n\nfunction toPassage(\n rawId: string,\n attrs: Record<string, string>,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n inlineScore?: string,\n): Passage {\n const score =\n scoreSource === 'lexical' ? Number(inlineScore ?? '0') : 1 - Number(attrs.vector_score ?? '1');\n const source = attrs.source;\n return {\n id: rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId,\n text: attrs.text ?? '',\n score: Number.isFinite(score) ? score : 0,\n ...(source !== undefined ? { source } : {}),\n ...parseMetadata(attrs.metadata_json),\n };\n}\n\nfunction parseMetadata(raw: string | undefined): { metadata?: Record<string, unknown> } {\n if (raw === undefined) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed === 'object' && parsed !== null) {\n return { metadata: parsed as Record<string, unknown> };\n }\n } catch {\n // not JSON — no metadata\n }\n return {};\n}\n","/**\n * What a retrieval was served BY — the static half of retrieval telemetry, separated from the\n * emitting half (`retrieval-telemetry.ts`) so a store or a retriever can describe itself without\n * importing anything that emits. That separation is not tidiness: `retrieval-telemetry.ts` imports\n * the retrievers' shapes to detect them, and the retrievers import their descriptor kind, so putting\n * both halves in one module would make every retriever ↔ telemetry edge a cycle.\n */\n\n/** Which retrieval strategy answered. `unknown` is a retriever this package did not ship. */\nexport type RetrieverKind =\n | 'embedding'\n | 'keyword'\n | 'lexical'\n | 'hybrid'\n | 'filtered'\n | 'reranking'\n | 'tree-navigation'\n | 'two-stage'\n | 'unknown';\n\n/** Which backend held the chunks. Omitted entirely for a retriever that holds its own (keyword). */\nexport type VectorStoreKind = 'memory' | 'pg' | 'redis';\n\n/**\n * A retriever's (or a store's) self-description, as it appears on a retrieval event. Every field is\n * optional because the describer is answering only for the layer it *is*: a store knows its backend\n * and namespace but not which strategy queried it; `KeywordRetriever` knows its strategy but has no\n * store at all.\n */\nexport interface RetrievalDescriptor {\n retriever?: RetrieverKind;\n store?: VectorStoreKind;\n /**\n * The namespace the chunks live in — `PgVectorStore`'s table, `RedisVectorStore`'s RediSearch\n * index. This is the STORE-level namespace, not a per-query metadata scope: a host that partitions\n * one index into logical collections with a metadata filter (the usual multi-tenant shape) reports\n * that through `InstrumentRetrieverOptions.collection`, which overrides this.\n */\n collection?: string;\n}\n\n/**\n * The optional capability a store or retriever implements to appear on a retrieval event as\n * something other than `unknown`. Deliberately NOT a member of `VectorStore` or of core's\n * `Retriever`: both are implemented outside this package, and adding a required member to either\n * would break every existing implementation on upgrade — for a field that only observability reads.\n */\nexport interface DescribesRetrieval {\n describeRetrieval(): RetrievalDescriptor;\n}\n\n/** Structural guard, so a `VectorStore`/`Retriever` is narrowed to the capability without a cast. */\nexport function describesRetrieval(value: unknown): value is DescribesRetrieval {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof Reflect.get(value, 'describeRetrieval') === 'function'\n );\n}\n\n/** A candidate's descriptor, or an empty one when it does not carry the capability. */\nexport function describeRetrieval(candidate: unknown): RetrievalDescriptor {\n return describesRetrieval(candidate) ? candidate.describeRetrieval() : {};\n}\n\n/**\n * The store half of a wrapped retriever's descriptor: the base's `store`/`collection` with its\n * `retriever` dropped, so the wrapper can stamp its own strategy on top.\n *\n * `new FilteredRetriever(new EmbeddingRetriever(embedder, pg), scope)` therefore reports\n * `{ retriever: 'filtered', store: 'pg', collection: 'agent_rag_chunks' }` rather than losing the\n * store: which backend answered is the thing a \"retrievals by store\" panel exists to show, and a\n * production wiring is almost always two or three wrappers deep.\n */\nexport function describeSource(base: unknown): Omit<RetrievalDescriptor, 'retriever'> {\n const { retriever: _ignored, ...source } = describeRetrieval(base);\n return source;\n}\n\n/**\n * The store half shared by EVERY leg of a composed retriever, or nothing when they disagree.\n *\n * A hybrid whose legs are the dense and lexical halves of the same RediSearch index (the shape\n * `LexicalRetriever` exists for) genuinely was served by that one store, and reporting it is what\n * makes the store breakdown add up. A hybrid fusing pgvector with an in-process BM25 index was not\n * served by either alone, and picking the first leg would silently attribute every such retrieval to\n * a store that answered half of it.\n */\nexport function describeSharedSource(\n legs: readonly unknown[],\n): Omit<RetrievalDescriptor, 'retriever'> {\n const sources = legs.map(describeSource);\n const [first] = sources;\n if (first === undefined) {\n return {};\n }\n const agrees = sources.every(\n (source) => source.store === first.store && source.collection === first.collection,\n );\n return agrees ? first : {};\n}\n","import type {\n EmbeddingProvider,\n Passage,\n RetrieveOptions,\n Retriever,\n} from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\nimport type { VectorStore } from './vector-store.js';\n\nexport interface EmbeddingRetrieverOptions {\n /**\n * Drop passages scoring below this, so a query the corpus cannot answer returns nothing.\n *\n * A vector search always returns its `topK` nearest neighbours, and \"nearest\" is not \"relevant\".\n * Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least\n * unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result\n * says the answer is absent, so without a floor the consumer either feeds a model five irrelevant\n * passages or invents a threshold of its own.\n *\n * **This is the only place in the package where a threshold is meaningful, which is why it lives\n * here and not on `HybridRetriever`.** The score here comes straight from the store's vector\n * search: a calibrated similarity, comparable across queries and monotonic in how well the passage\n * matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each\n * leg, then fuse; never the other way round.\n *\n * Pick the number by measuring, not by taste: score questions you know the corpus answers against\n * questions you know it does not, and put the floor in the gap between the two populations. It is\n * specific to the embedding model and the distance metric, so it does not survive changing either.\n * Left unset nothing is dropped, which is the previous behaviour exactly.\n */\n minScore?: number;\n}\n\n/**\n * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:\n * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool\n * via `createRetrievalTool`, or into `AgentModule.forRoot({ retrieval })` for inject mode).\n */\nexport class EmbeddingRetriever implements Retriever {\n constructor(\n private readonly embedder: EmbeddingProvider,\n private readonly store: VectorStore,\n private readonly options: EmbeddingRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the dense strategy, over whatever store it was handed. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'embedding', ...describeSource(this.store) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const [embedding] = await this.embedder.embed([query]);\n if (embedding === undefined) {\n return [];\n }\n const passages = await this.store.search(embedding, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n const { minScore } = this.options;\n // Applied here rather than pushed into the store's query: not every backend can express \"score\n // above X\" in the same call as its KNN search, and filtering after keeps the floor byte-identical\n // across all three adapters instead of subtly engine-specific. It also keeps `topK` meaning \"ask\n // the store for this many\", so a floor never silently turns into a smaller search.\n return minScore === undefined\n ? passages\n : passages.filter((passage) => passage.score >= minScore);\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport type { ChunkRecord } from './ingest.js';\nimport type { RetrievalDescriptor } from './retrieval-descriptor.js';\nimport { documentIdOf } from './vector-store.js';\n\nexport interface KeywordRetrieverOptions {\n /** BM25 term-frequency saturation. Default 1.5. */\n k1?: number;\n /** BM25 length normalization, 0 (off) … 1 (full). Default 0.75. */\n b?: number;\n}\n\ninterface IndexedDoc {\n record: ChunkRecord;\n /** term → frequency within this doc. */\n frequencies: Map<string, number>;\n length: number;\n}\n\n/**\n * An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME\n * {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids\n * line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation\n * (`k1`), and document-length normalization (`b`).\n *\n * Because it holds its OWN copy of each chunk's text, it needs the same delete-sync as the vector\n * store: whenever you call {@link import('./vector-store.js').VectorStore.remove}, call\n * {@link KeywordRetriever.remove} with the same document id. Skipping it leaves the document\n * lexically retrievable — `retrieve` would keep returning the removed passage's full text.\n */\nexport class KeywordRetriever implements Retriever {\n private readonly docs = new Map<string, IndexedDoc>();\n private readonly documentFrequency = new Map<string, number>();\n private totalLength = 0;\n private readonly k1: number;\n private readonly b: number;\n\n constructor(options: KeywordRetrieverOptions = {}) {\n this.k1 = options.k1 ?? 1.5;\n this.b = options.b ?? 0.75;\n }\n\n /**\n * Telemetry self-description. No `store`: this retriever holds its own copy of the corpus in\n * process, so attributing its retrievals to whichever vector store happens to hold the same chunks\n * would credit a backend that was never queried.\n */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'keyword' };\n }\n\n /** How many chunks are currently indexed. */\n get size(): number {\n return this.docs.size;\n }\n\n /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */\n add(records: ChunkRecord[]): void {\n for (const record of records) {\n this.removeChunk(record.id);\n const tokens = tokenize(record.text);\n const frequencies = new Map<string, number>();\n for (const token of tokens) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + 1);\n }\n for (const term of frequencies.keys()) {\n this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1);\n }\n this.docs.set(record.id, { record, frequencies, length: tokens.length });\n this.totalLength += tokens.length;\n }\n }\n\n /**\n * Forget a source document — every chunk whose id is `${documentId}` or `${documentId}#<n>`, the\n * same id scheme (and the same {@link documentIdOf} collapse) the vector stores use for their\n * `remove`. The keyword half of a hybrid index keeps its own copy of the chunk text, so a document\n * dropped from the vector store stays fully retrievable here until this is called. Unknown ids are\n * a no-op.\n */\n remove(documentId: string): void {\n for (const id of [...this.docs.keys()]) {\n if (id === documentId || documentIdOf(id) === documentId) {\n this.removeChunk(id);\n }\n }\n }\n\n /** Drop every indexed chunk, resetting the corpus statistics with it. */\n clear(): void {\n this.docs.clear();\n this.documentFrequency.clear();\n this.totalLength = 0;\n }\n\n /** Drop ONE chunk by its exact id, decrementing the corpus statistics it contributed. */\n private removeChunk(id: string): void {\n const existing = this.docs.get(id);\n if (existing === undefined) {\n return;\n }\n for (const term of existing.frequencies.keys()) {\n const next = (this.documentFrequency.get(term) ?? 1) - 1;\n if (next <= 0) {\n this.documentFrequency.delete(term);\n } else {\n this.documentFrequency.set(term, next);\n }\n }\n this.totalLength -= existing.length;\n this.docs.delete(id);\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const total = this.docs.size;\n if (total === 0) {\n return [];\n }\n const averageLength = this.totalLength / total;\n const queryTerms = new Set(tokenize(query));\n const scored: Passage[] = [];\n for (const { record, frequencies, length } of this.docs.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n let score = 0;\n for (const term of queryTerms) {\n const termFrequency = frequencies.get(term);\n if (termFrequency === undefined) {\n continue;\n }\n const df = this.documentFrequency.get(term) ?? 0;\n const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5));\n const denominator =\n termFrequency + this.k1 * (1 - this.b + (this.b * length) / averageLength);\n score += idf * ((termFrequency * (this.k1 + 1)) / denominator);\n }\n if (score > 0) {\n scored.push({\n id: record.id,\n text: record.text,\n score,\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK ?? 5);\n }\n}\n\nfunction tokenize(text: string): string[] {\n return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\nimport type { LexicalVectorStore } from './vector-store.js';\n\n/**\n * The keyword half of hybrid search, served by the store's own full-text index — the mirror of\n * {@link import('./embedding-retriever.js').EmbeddingRetriever}, which serves the dense half. It\n * holds no state: every query goes to the store, so any process that can reach the store can run\n * lexical search, whether or not it did the ingestion.\n *\n * Compose it exactly like the dense leg:\n *\n * ```ts\n * const retriever = new HybridRetriever([\n * new EmbeddingRetriever(embedder, store),\n * new LexicalRetriever(store),\n * ]);\n * ```\n *\n * Both legs read the same chunks from the same index, so their ids line up by construction and\n * `HybridRetriever`'s RRF fuses them without either side needing a score on a common scale.\n *\n * Prefer this over {@link import('./keyword-retriever.js').KeywordRetriever} whenever the store\n * supports it (see {@link import('./vector-store.js').isLexicalVectorStore}) and ingestion and\n * search can run in different processes; `KeywordRetriever` remains the answer for `MemoryVectorStore`\n * and single-process deployments.\n */\nexport class LexicalRetriever implements Retriever {\n constructor(private readonly store: LexicalVectorStore) {}\n\n /** Telemetry self-description — the store's own lexical index, so the store is the same one. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'lexical', ...describeSource(this.store) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.store.searchText(query, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSharedSource } from './retrieval-descriptor.js';\n\nexport interface HybridRetrieverOptions {\n /** RRF constant — larger flattens the rank weighting. Default 60 (the value from the original paper). */\n k?: number;\n /** Candidates pulled from each retriever before fusion. Default 20. */\n fetchTopK?: number;\n /** Per-retriever multipliers on each RRF contribution (same order as `retrievers`). Default all 1. */\n weights?: number[];\n}\n\n/**\n * Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense\n * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the\n * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs\n * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.\n *\n * ## The score on a fused passage is not a similarity. Do not threshold it.\n *\n * Read this before using `passage.score` for anything other than the order it already produced.\n *\n * Not normalizing between scales is what makes RRF robust, and the price is that the number it\n * emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that\n * bite in practice:\n *\n * - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though\n * it arrives in the same field, of the same type, from the same `Retriever` interface. There is no\n * discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you\n * surface scores to a user or a model, know which retriever produced them.\n * - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in\n * `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,\n * `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the\n * least-bad member of a set of terrible matches both come back around `0.016`.\n *\n * The second point is why a relevance floor cannot live here. What the fused score actually measures\n * is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs\n * return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the\n * same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone\n * separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same\n * queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not\n * appear; what does not change is that both numbers live in a band roughly one part in fifty wide,\n * with no room for a threshold to mean anything.\n *\n * So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what\n * survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here\n * would be an invitation to apply it to a number that cannot support one.\n */\nexport class HybridRetriever implements Retriever {\n constructor(\n private readonly retrievers: Retriever[],\n private readonly options: HybridRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'hybrid', ...describeSharedSource(this.retrievers) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const k = this.options.k ?? 60;\n const fetchTopK = this.options.fetchTopK ?? 20;\n const weights = this.options.weights;\n const lists = await Promise.all(\n this.retrievers.map((retriever) =>\n retriever.retrieve(query, {\n topK: fetchTopK,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n }),\n ),\n );\n\n const fused = new Map<string, { passage: Passage; score: number }>();\n lists.forEach((list, listIndex) => {\n const weight = weights?.[listIndex] ?? 1;\n list.forEach((passage, rank) => {\n const contribution = weight / (k + rank + 1);\n const existing = fused.get(passage.id);\n if (existing !== undefined) {\n existing.score += contribution;\n } else {\n fused.set(passage.id, { passage, score: contribution });\n }\n });\n });\n\n return [...fused.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, options.topK ?? 5)\n .map((entry) => ({ ...entry.passage, score: entry.score }));\n }\n}\n","import type { Passage, Reranker, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\n\nexport interface RerankingRetrieverOptions {\n /** How many candidates to pull from the base retriever before reranking. Default 20. */\n fetchTopK?: number;\n}\n\n/**\n * Two-stage retrieval: over-fetch cheap candidates from a base `Retriever`, then reorder them with a\n * stronger {@link Reranker} and keep the top few. The standard precision boost — a fast first stage\n * casts a wide net, a slow accurate second stage sharpens it. Composes over ANY retriever (vector,\n * hybrid, keyword), and is itself a `Retriever`.\n */\nexport class RerankingRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly reranker: Reranker,\n private readonly options: RerankingRetrieverOptions = {},\n ) {}\n\n /** Telemetry self-description — the two-stage strategy, over the base's store. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'reranking', ...describeSource(this.base) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const candidates = await this.base.retrieve(query, {\n topK: this.options.fetchTopK ?? 20,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n return this.reranker.rerank(query, candidates, { topK: options.topK ?? 5 });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { type RetrievalDescriptor, describeSource } from './retrieval-descriptor.js';\n\n/**\n * Wraps a `Retriever` so every `retrieve` call ANDs a fixed metadata `filter` on top of whatever the\n * caller passes — the owner/tenant-scoping primitive. `new FilteredRetriever(base, { ownerId })` only\n * ever returns that owner's passages, so a shared vector store serves per-user / per-tenant RAG\n * without the agent (or a crafted query) reaching across the boundary.\n *\n * The fixed filter takes precedence on key conflicts: a caller can narrow further but cannot widen\n * past the scope. Composes over ANY retriever (vector, hybrid, reranked) and is itself a `Retriever`.\n */\nexport class FilteredRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly filter: Record<string, unknown>,\n ) {}\n\n /** Telemetry self-description — this scope, over the base's store. */\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'filtered', ...describeSource(this.base) };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.base.retrieve(query, {\n ...options,\n filter: { ...options.filter, ...this.filter },\n });\n }\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\nimport { type ChunkOptions, chunkText } from './chunk.js';\nimport type { VectorRecord, VectorStore } from './vector-store.js';\n\n/** A source document to ingest. `id` scopes the chunk ids (`${id}#${n}`); `source` is the citation. */\nexport interface IngestDocument {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\n/** A chunked, not-yet-embedded record. Feed the same array to a vector store AND a keyword index so\n * both retrievers key on identical chunk ids (which is what lets hybrid fusion line them up). */\nexport interface ChunkRecord {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface IngestChunksOptions {\n embedder: EmbeddingProvider;\n store: VectorStore;\n}\n\nexport interface IngestOptions extends ChunkOptions, IngestChunksOptions {}\n\n/**\n * Split documents into chunk records. Chunk ids are `${doc.id}#${index}`, so re-chunking the same\n * document produces the same ids (upsert overwrites in place rather than duplicating).\n */\nexport function chunkDocuments(\n documents: IngestDocument[],\n options: ChunkOptions = {},\n): ChunkRecord[] {\n const chunks: ChunkRecord[] = [];\n for (const document of documents) {\n chunkText(document.text, options).forEach((text, index) => {\n chunks.push({\n id: `${document.id}#${index}`,\n text,\n ...(document.source !== undefined ? { source: document.source } : {}),\n ...(document.metadata !== undefined ? { metadata: document.metadata } : {}),\n });\n });\n }\n return chunks;\n}\n\n/** Embed pre-chunked records (one batched `embed`) and upsert them. Returns the record count. */\nexport async function ingestChunks(\n chunks: ChunkRecord[],\n options: IngestChunksOptions,\n): Promise<number> {\n if (chunks.length === 0) {\n return 0;\n }\n const embeddings = await options.embedder.embed(chunks.map((chunk) => chunk.text));\n const records: VectorRecord[] = chunks.map((chunk, index) => ({\n ...chunk,\n embedding: embeddings[index] ?? [],\n }));\n await options.store.upsert(records);\n return records.length;\n}\n\n/** Chunk → embed → upsert in one call. For hybrid search, use {@link chunkDocuments} +\n * {@link ingestChunks} and feed the same chunks to a {@link import('./keyword-retriever.js').KeywordRetriever}. */\nexport async function ingestDocuments(\n documents: IngestDocument[],\n options: IngestOptions,\n): Promise<number> {\n return ingestChunks(chunkDocuments(documents, options), options);\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { channelName, emit, getChannel } from '@dudousxd/nestjs-diagnostics';\nimport {\n type RetrievalDescriptor,\n type RetrieverKind,\n type VectorStoreKind,\n describeRetrieval,\n} from './retrieval-descriptor.js';\n\n/**\n * Retrieval telemetry: one diagnostics event per retrieval, carrying how long it took, how much came\n * back, how good it was, and what served it.\n *\n * Until this existed, retrieval reached Telescope only as the *tool call* that wrapped it (see\n * `retrieval-tool.ts`), which says a retrieval happened and nothing else. The questions an operator\n * actually arrives with — is retrieval slow, is it returning nothing, is one collection colder than\n * the rest, did the scores collapse after a model swap — are all invisible in a tool-call row,\n * because a tool call that returns zero passages and one that returns five perfect ones are the same\n * row with the same `ok` status.\n *\n * ## One seam, not six\n *\n * Instrumentation is a WRAPPER over the core `Retriever` SPI ({@link instrumentRetriever}), not code\n * inside each retriever. Every retriever in this package — and every retriever outside it — is the\n * same one-method interface, so one wrapper covers all of them, including ones this package has\n * never heard of. The alternative, an `emit` inside each of the six `retrieve` methods, would have\n * had to be repeated in each, skipped by third-party retrievers entirely, and would have double- and\n * triple-counted the composed ones (a hybrid over two legs emitting three events for one retrieval).\n *\n * ## How a composed retrieval reports\n *\n * **The outermost instrumented retriever in a call tree emits; anything instrumented inside it stays\n * silent.** A retrieval is one question the caller asked, so it is one event no matter how many\n * retrievers cooperated to answer it: `RerankingRetriever` over a `HybridRetriever` over a dense and\n * a lexical leg is ONE retrieval, reported as `retriever: 'reranking'` with the duration of the\n * whole thing and the scores the caller actually got back.\n *\n * The suppression is enforced with an {@link AsyncLocalStorage} flag rather than by asking hosts to\n * wrap carefully, because the wrapping that double-counts is the natural one: a host that wraps its\n * top-level retriever and *also* wraps the dense leg it shares with another code path has not made a\n * mistake, and would otherwise get two events per retrieval, a zero-hit rate computed over a mixture\n * of legs and composites, and a retrieval count roughly twice its query count.\n *\n * ## Opt-out safety, and what the hot path pays\n *\n * The event goes out on `aviary:rag:retrieval` via `@dudousxd/nestjs-diagnostics`, whose `emit` is\n * gated on `channel.hasSubscribers` and never throws. This wrapper checks that same gate BEFORE it\n * does anything at all — no clock read, no `AsyncLocalStorage.run`, no score arithmetic — and\n * delegates straight through when nothing is listening. A host that wires this package without\n * Telescope (or any subscriber) pays two `Map` lookups and a boolean per retrieval, and nothing else\n * changes: the wrapper returns the base retriever's passages verbatim, and rethrows its errors\n * unchanged.\n *\n * ## Why this channel is not merged into the typed `ChannelRegistry`\n *\n * `@dudousxd/nestjs-agent-rag-media` already emits `media.ingested`/`media.removed`/`media.skipped`/\n * `media.failed` on the same `rag` lib, and flip's host app emits `rag:search` on it. Declaring\n * `ChannelRegistry['rag'] = { retrieval: RagRetrievalEvent }` here would narrow `EventOf<'rag'>` to\n * this one key process-wide and turn every one of those sibling emits into a compile error in any\n * project that installs both packages. The payload stays typed where it matters — {@link emitRagRetrieval}\n * takes {@link RagRetrievalEvent} — and the wire contract is documented below instead.\n */\n\n/** The diagnostics lib segment. Shared with `@dudousxd/nestjs-agent-rag-media`'s `media.*` events. */\nexport const RAG_DIAGNOSTIC_LIB = 'rag';\n\n/** The diagnostics event segment. */\nexport const RAG_RETRIEVAL_EVENT = 'retrieval';\n\n/**\n * The full channel name a subscriber listens on: `aviary:rag:retrieval`. This is the wire contract —\n * `@dudousxd/nestjs-agent-telescope` subscribes to this exact string rather than importing this\n * package, which is what keeps the telescope extension free of a dependency on the retrieval stack\n * (and lets a host that uses neither still observe retrieval with a bare `diagnostics_channel`\n * subscription).\n */\nexport const RAG_RETRIEVAL_CHANNEL = channelName(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT);\n\n/**\n * The payload of one retrieval event. Counts, durations, kinds and scores only — never the query\n * text and never passage text. Both are user content, and this lands in an ops store with a short\n * retention window and a Slack-facing redaction budget; everything here is safe to put on a\n * dashboard and is what tells the failure modes apart anyway.\n *\n * The DURATION is not in here: it rides the envelope (`emit`'s `opts.durationMs`), which is the field\n * Telescope's OTel exporter turns into a latency histogram. Putting it in the payload instead would\n * have made retrieval latency a number somebody reads out of a JSON blob rather than a graph.\n */\nexport interface RagRetrievalEvent {\n /** Which strategy answered — the OUTERMOST one, for a composed retriever. */\n retriever: RetrieverKind;\n /** Which backend held the chunks. Absent for an in-process retriever, or when legs disagree. */\n store?: VectorStoreKind;\n /** Store namespace (pg table / RediSearch index), or the host's logical collection. */\n collection?: string;\n /** What the caller asked for, so `chunks < topK` is readable as \"the corpus ran out\". */\n topK: number;\n /** How many passages came back. */\n chunks: number;\n /**\n * `chunks === 0`. Carried as its own flag rather than left to be derived, because the zero-hit RATE\n * is the headline number and deriving it means every consumer re-deriving the same predicate.\n */\n zeroHit: boolean;\n /** Best score in the result, rounded to 4dp. Absent on a zero-hit. */\n topScore?: number;\n /** Mean score across the result, rounded to 4dp. Absent on a zero-hit. */\n meanScore?: number;\n /**\n * The retrieval threw. Reported rather than dropped because a failed retrieval and an empty one\n * look identical from the agent's side — both become \"I couldn't find anything\" — so a channel\n * that only carried successes would go quiet exactly when retrieval broke.\n */\n failed: boolean;\n /** The error message, when `failed`. */\n error?: string;\n}\n\n/** Publish one retrieval event. `durationMs` rides the envelope; see {@link RagRetrievalEvent}. */\nexport function emitRagRetrieval(event: RagRetrievalEvent, durationMs: number): void {\n emit(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT, event, { durationMs });\n}\n\nexport interface InstrumentRetrieverOptions {\n /**\n * Override the auto-detected {@link RetrievalDescriptor}, field by field. Needed for a retriever or\n * store from outside this package (which reports `retriever: 'unknown'` and no store), and for a\n * host that wants its own naming.\n */\n describe?: RetrievalDescriptor;\n /**\n * The logical collection this retrieval searched, when the store namespace is not it. Pass a\n * function to read it off the call's metadata filter — the multi-tenant shape, where one index\n * holds every collection and the filter picks one:\n *\n * ```ts\n * instrumentRetriever(retriever, {\n * collection: (filter) => (typeof filter?.collectionId === 'string' ? filter.collectionId : undefined),\n * })\n * ```\n *\n * Returning `undefined` falls back to the store namespace, which is the right answer for a query\n * scoped to many collections at once: a breakdown panel needs ONE label per retrieval, and\n * inventing a synthetic `\"a+b+c\"` bucket would produce a chart whose slices are combinations\n * rather than collections.\n */\n collection?: string | ((filter: Record<string, unknown> | undefined) => string | undefined);\n}\n\n/**\n * The default a retriever falls back to when the caller names no `topK` — every retriever in this\n * package uses `options.topK ?? 5`, so reporting anything else here would make `chunks < topK`\n * misread as truncation on exactly the calls that did not ask for a limit.\n */\nconst DEFAULT_TOP_K = 5;\n\n/**\n * Set for the duration of an instrumented `retrieve`, so an instrumented retriever running INSIDE\n * another one knows it is a leg and stays silent. See the composed-retrieval section above.\n */\nconst inRetrieval = new AsyncLocalStorage<true>();\n\n/** Round to 4dp — enough to see a score distribution move, short enough to keep the payload small. */\nfunction round4(value: number): number {\n return Math.round(value * 10_000) / 10_000;\n}\n\n/** Top + mean score, skipping any passage whose `score` is not a finite number. */\nfunction scoreStats(passages: Passage[]): { topScore?: number; meanScore?: number } {\n const scores = passages.map((passage) => passage.score).filter((score) => Number.isFinite(score));\n if (scores.length === 0) {\n return {};\n }\n const sum = scores.reduce((total, score) => total + score, 0);\n return { topScore: round4(Math.max(...scores)), meanScore: round4(sum / scores.length) };\n}\n\nclass InstrumentedRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly options: InstrumentRetrieverOptions = {},\n ) {}\n\n /** Transparent to detection: wrapping a retriever must not turn it into an `unknown` one. */\n describeRetrieval(): RetrievalDescriptor {\n return { ...describeRetrieval(this.base), ...this.options.describe };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n // Both gates before any work: nothing subscribed, or we are a leg of an outer instrumented\n // retrieval that will report the whole thing itself.\n if (!getChannel(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT).hasSubscribers) {\n return this.base.retrieve(query, options);\n }\n if (inRetrieval.getStore() === true) {\n return this.base.retrieve(query, options);\n }\n\n const descriptor = this.describeRetrieval();\n const topK = options.topK ?? DEFAULT_TOP_K;\n const startedAt = Date.now();\n try {\n const passages = await inRetrieval.run(true, () => this.base.retrieve(query, options));\n emitRagRetrieval(\n {\n retriever: descriptor.retriever ?? 'unknown',\n ...(descriptor.store !== undefined ? { store: descriptor.store } : {}),\n ...this.collectionOf(options.filter, descriptor),\n topK,\n chunks: passages.length,\n zeroHit: passages.length === 0,\n ...scoreStats(passages),\n failed: false,\n },\n Date.now() - startedAt,\n );\n return passages;\n } catch (error) {\n emitRagRetrieval(\n {\n retriever: descriptor.retriever ?? 'unknown',\n ...(descriptor.store !== undefined ? { store: descriptor.store } : {}),\n ...this.collectionOf(options.filter, descriptor),\n topK,\n chunks: 0,\n zeroHit: true,\n failed: true,\n error: error instanceof Error ? error.message : String(error),\n },\n Date.now() - startedAt,\n );\n throw error;\n }\n }\n\n /** The host's collection for this call, else the store namespace, else nothing. */\n private collectionOf(\n filter: Record<string, unknown> | undefined,\n descriptor: RetrievalDescriptor,\n ): { collection?: string } {\n const { collection } = this.options;\n const resolved =\n typeof collection === 'function' ? collection(filter) : (collection ?? descriptor.collection);\n return resolved !== undefined ? { collection: resolved } : {};\n }\n}\n\n/**\n * Wrap a retriever so every `retrieve` publishes a {@link RagRetrievalEvent} on\n * {@link RAG_RETRIEVAL_CHANNEL}. Wrap the retriever you actually hand to the agent — the outermost\n * one — and nothing else needs changing:\n *\n * ```ts\n * const retriever = instrumentRetriever(\n * new HybridRetriever([new EmbeddingRetriever(embedder, store), new LexicalRetriever(store)]),\n * );\n * ```\n *\n * {@link createRetrievalTool} already does this for the tool it builds, so agentic retrieval is\n * instrumented with no host change at all; call this directly for inject-mode retrieval\n * (`AgentModule.forRoot({ retrieval })`) or for a host that calls `retrieve` itself. Wrapping twice\n * is harmless — see the composed-retrieval section above.\n */\nexport function instrumentRetriever(\n retriever: Retriever,\n options: InstrumentRetrieverOptions = {},\n): Retriever {\n return new InstrumentedRetriever(retriever, options);\n}\n","import type { Passage, Retriever, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { type InstrumentRetrieverOptions, instrumentRetriever } from './retrieval-telemetry.js';\n\nexport interface RetrievalToolOptions {\n /** Tool name the model sees. Default `search_knowledge`. */\n name?: string;\n description?: string;\n /** Passages per search. Default lets the retriever decide (typically 5). */\n topK?: number;\n /**\n * Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store\n * and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic\n * mode: instrumenting here is what makes the RAG panels work for a host that only ever calls\n * `createRetrievalTool`, with no wiring at all. It costs nothing when nobody subscribes (see\n * {@link instrumentRetriever}), and cannot double-count when the host has already wrapped the\n * retriever itself.\n *\n * Pass `false` to opt a single tool out — a host that reports retrieval its own way, or one whose\n * corpus is small enough that the events are noise.\n */\n telemetry?: boolean | InstrumentRetrieverOptions;\n}\n\n/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */\nexport interface RetrievalTool {\n spec: ToolSpec;\n handler: ToolHandler;\n}\n\nfunction isQueryInput(value: unknown): value is { query: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'query' in value &&\n typeof (value as Record<'query', unknown>).query === 'string'\n );\n}\n\n/** A hand-rolled Standard Schema for `{ query: string }` — keeps this package validation-lib-free. */\nconst querySchema: StandardSchemaV1<{ query: string }> = {\n '~standard': {\n version: 1,\n vendor: 'nestjs-agent-rag',\n validate(value: unknown) {\n return isQueryInput(value)\n ? { value: { query: value.query } }\n : { issues: [{ message: 'retrieval tool expects { query: string }' }] };\n },\n },\n};\n\n/**\n * Builds the agentic-retrieval tool: a `read`-kind `search_knowledge(query)` the model calls when it\n * decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources\n * flow to the frontend and telescope through the normal tool-call machinery (the citation surface).\n *\n * The retrieval ITSELF is also instrumented (`aviary:rag:retrieval`) unless `telemetry: false` — the\n * tool-call row says a retrieval happened, the retrieval event says how long it took, how much came\n * back and how good it was. See {@link instrumentRetriever}.\n *\n * ```ts\n * providers: [provideAgentTool(createRetrievalTool(retriever))]\n * ```\n */\nexport function createRetrievalTool(\n retriever: Retriever,\n options: RetrievalToolOptions = {},\n): RetrievalTool {\n const topK = options.topK;\n const { telemetry = true } = options;\n const instrumented =\n telemetry === false\n ? retriever\n : instrumentRetriever(retriever, telemetry === true ? {} : telemetry);\n return {\n spec: {\n name: options.name ?? 'search_knowledge',\n kind: 'read',\n description:\n options.description ??\n 'Search the knowledge base for passages relevant to a query. Returns passages with their source for citation.',\n inputSchema: querySchema,\n },\n handler: {\n async execute(input: unknown): Promise<{ passages: Passage[] }> {\n if (!isQueryInput(input)) {\n throw new Error('retrieval tool expects { query: string }');\n }\n const passages = await instrumented.retrieve(\n input.query,\n topK !== undefined ? { topK } : {},\n );\n return { passages };\n },\n },\n };\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\n\n/** A non-2xx answer (or a malformed one) from an embeddings or rerank endpoint. */\nexport class HttpModelError extends Error {\n constructor(\n /** HTTP status; `502` when the endpoint answered 2xx with a body that doesn't fit the contract. */\n readonly status: number,\n message: string,\n ) {\n super(message);\n this.name = 'HttpModelError';\n }\n}\n\nexport interface OpenAiEmbeddingsOptions {\n /** The embedding model id, sent as `model`. */\n model: string;\n /**\n * Base URL up to and including `/v1` — the client posts to `<baseUrl>/embeddings`. Default\n * `https://api.openai.com/v1`. Point it at any OpenAI-compatible server: Azure/OpenRouter/an LLM\n * gateway, Ollama (`http://localhost:11434/v1`), Hugging Face TEI, vLLM, LocalAI, LM Studio…\n */\n baseUrl?: string;\n /** Sent as `Authorization: Bearer <apiKey>` when set. Local servers usually need none. */\n apiKey?: string;\n /** Output width for models that support shortening (`text-embedding-3-*`), sent as `dimensions`. */\n dimensions?: number;\n /**\n * Inputs per request. Default 64. When the server refuses a request for its size (see\n * {@link isBatchTooLarge}) the batch is split in halves and retried, and later requests use the\n * size that worked — so a server whose limit is below this (TEI's `--max-client-batch-size`\n * defaults to 32) costs one refused request, not a failed ingestion.\n */\n batchSize?: number;\n /**\n * Inputs longer than this many characters are truncated before sending, so one oversized chunk\n * doesn't fail a whole batch on the model's context limit. Default 24 000 (≈ 6–8k tokens); set\n * `Infinity` to send inputs as they are.\n */\n maxInputChars?: number;\n /** Extra request headers (an org/project id, a gateway's attribution header). */\n headers?: Record<string, string>;\n /** Per-request timeout. Default 120 000 ms. */\n timeoutMs?: number;\n /** Called with the provider-reported token count of every request — for cost accounting. */\n onUsage?: (tokens: number) => void;\n /**\n * Called when the server refused a batch for its size and it was split — a misconfiguration worth\n * surfacing: raise the server's limit (TEI: `--max-client-batch-size`) or lower `batchSize`.\n */\n onWarn?: (message: string) => void;\n /** `fetch` to use. Default the global one. */\n fetch?: typeof fetch;\n}\n\n/**\n * Did the server refuse this request for its **size** — too many inputs (TEI's\n * `--max-client-batch-size`: `batch size 64 > maximum allowed batch size 32`) or too large a\n * payload? Those are fixed by sending fewer inputs per request; anything else (auth, a bad model,\n * one input over the context window) is not, and is rethrown as is.\n */\nexport function isBatchTooLarge(error: unknown): boolean {\n if (!(error instanceof HttpModelError)) {\n return false;\n }\n if (error.status === 413) {\n return true;\n }\n return (\n (error.status === 400 || error.status === 422) &&\n /batch.?size|too many inputs|maximum allowed batch|payload too large|max.*(inputs|batch)/i.test(\n error.message,\n )\n );\n}\n\n/**\n * Batch sizes a server accepted after refusing larger ones, per endpoint URL and model, for the\n * life of the process — so a second provider built for the same server (one per request, say)\n * starts at the size that works instead of re-learning it with a refused request.\n */\nconst learnedBatchLimits = new Map<string, number>();\n\n/**\n * An {@link EmbeddingProvider} over any OpenAI-compatible `POST /v1/embeddings` endpoint, with no SDK\n * dependency: OpenAI itself, or a gateway/local server speaking the same protocol (TEI, Ollama,\n * vLLM, LocalAI…). For a Vercel AI SDK model use `aiSdkEmbedding` from `-ai-sdk` instead.\n *\n * Batches by {@link OpenAiEmbeddingsOptions.batchSize} — splitting and retrying a batch the server\n * refuses as too large, and remembering the size that worked — restores input order from each item's\n * `index`, and fails loudly (an {@link HttpModelError}) on a non-2xx answer or a vector count that\n * doesn't match the inputs. Empty strings are sent as a single space, since OpenAI rejects them.\n *\n * ```ts\n * const embedder = openAiEmbeddings({ model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY });\n * const local = openAiEmbeddings({ baseUrl: 'http://tei:8080/v1', model: 'BAAI/bge-m3' });\n * ```\n */\nexport function openAiEmbeddings(options: OpenAiEmbeddingsOptions): EmbeddingProvider {\n const url = `${(options.baseUrl ?? 'https://api.openai.com/v1').replace(/\\/+$/, '')}/embeddings`;\n const limitKey = `${url} ${options.model}`;\n const configured = Math.max(1, Math.floor(options.batchSize ?? 64));\n let batchSize = Math.min(configured, learnedBatchLimits.get(limitKey) ?? configured);\n const maxInputChars = options.maxInputChars ?? 24_000;\n const timeoutMs = options.timeoutMs ?? 120_000;\n const doFetch = options.fetch ?? fetch;\n\n const request = async (input: string[]): Promise<number[][]> => {\n const response = await doFetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n input,\n ...(options.dimensions !== undefined ? { dimensions: options.dimensions } : {}),\n }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n const body = await response.text();\n if (!response.ok) {\n throw new HttpModelError(\n response.status,\n `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`,\n );\n }\n const json = parseJson(body) as {\n data?: { embedding?: unknown; index?: number }[];\n usage?: { prompt_tokens?: number; total_tokens?: number };\n } | null;\n const items = [...(json?.data ?? [])].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));\n if (items.length !== input.length || !items.every((item) => Array.isArray(item.embedding))) {\n throw new HttpModelError(\n 502,\n `Embeddings returned ${items.length} vectors for ${input.length} inputs`,\n );\n }\n const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;\n if (typeof tokens === 'number' && Number.isFinite(tokens)) {\n options.onUsage?.(tokens);\n }\n return items.map((item) => item.embedding as number[]);\n };\n\n // A batch refused for its size is split in halves (recursively) and retried; the smaller size\n // becomes the batch size for every later request to this server and model.\n const requestSplitting = async (input: string[]): Promise<number[][]> => {\n if (input.length > batchSize) {\n // The size was learned while this batch was being split: send the rest at the size that works.\n const vectors: number[][] = [];\n for (let start = 0; start < input.length; start += batchSize) {\n vectors.push(...(await requestSplitting(input.slice(start, start + batchSize))));\n }\n return vectors;\n }\n try {\n return await request(input);\n } catch (error) {\n if (!isBatchTooLarge(error) || input.length <= 1) {\n throw error;\n }\n const half = Math.ceil(input.length / 2);\n if (half < batchSize) {\n batchSize = half;\n learnedBatchLimits.set(limitKey, half);\n options.onWarn?.(\n `embeddings server ${url} refused a batch of ${input.length} inputs (${(error as Error).message}); retrying in batches of ${half}. Raise the server's batch limit (TEI: --max-client-batch-size) or lower batchSize.`,\n );\n }\n return [\n ...(await requestSplitting(input.slice(0, half))),\n ...(await requestSplitting(input.slice(half))),\n ];\n }\n };\n\n return {\n async embed(texts: string[]): Promise<number[][]> {\n const vectors: number[][] = [];\n for (let start = 0; start < texts.length; ) {\n const input = texts\n .slice(start, start + batchSize)\n .map(\n (text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || ' ',\n );\n vectors.push(...(await requestSplitting(input)));\n start += input.length;\n }\n return vectors;\n },\n };\n}\n\nfunction parseJson(body: string): unknown {\n try {\n return JSON.parse(body);\n } catch {\n return null;\n }\n}\n\n/** The human part of an error body: OpenAI's `error.message`, a bare `message`/`error`, or the text. */\nexport function errorMessageOf(body: string): string {\n const json = parseJson(body) as {\n error?: { message?: unknown } | string;\n message?: unknown;\n } | null;\n const message =\n typeof json?.error === 'string'\n ? json.error\n : (json?.error?.message ?? json?.message ?? undefined);\n return typeof message === 'string' ? message : body.slice(0, 300);\n}\n","import type { Passage, RerankOptions, Reranker } from '@dudousxd/nestjs-agent-core';\nimport { HttpModelError, errorMessageOf } from './openai-embeddings.js';\n\nexport interface HttpRerankerOptions {\n /**\n * The full rerank endpoint: `https://api.cohere.com/v2/rerank`, `https://api.jina.ai/v1/rerank`,\n * `https://api.voyageai.com/v1/rerank`, or a Hugging Face TEI / Infinity / vLLM server's `/rerank`.\n */\n url: string;\n /** Model id, sent as `model` (hosted APIs require it; TEI serves one model and ignores it). */\n model?: string;\n /** Sent as `Authorization: Bearer <apiKey>` when set. */\n apiKey?: string;\n /**\n * Request body dialect. `cohere` (Cohere, Jina, Voyage, Infinity, vLLM): `{ query, documents,\n * top_n }`. `tei`: `{ query, texts }`. Default `auto`, which sends `documents` **and** `texts` so\n * one config works against both families. Responses are read in every dialect regardless.\n */\n format?: 'auto' | 'cohere' | 'tei';\n /** Extra request headers. */\n headers?: Record<string, string>;\n /** Per-request timeout. Default 30 000 ms. */\n timeoutMs?: number;\n /** `fetch` to use. Default the global one. */\n fetch?: typeof fetch;\n}\n\ninterface RerankItem {\n index?: number;\n relevance_score?: number;\n score?: number;\n}\n\n/**\n * A cross-encoder {@link Reranker} behind an HTTP `/rerank` endpoint — the Cohere-style contract\n * most providers and self-hosted servers share. Compose it with `RerankingRetriever`:\n *\n * ```ts\n * const reranker = new HttpReranker({ url: 'https://api.cohere.com/v2/rerank', model: 'rerank-v3.5', apiKey });\n * const retriever = new RerankingRetriever(hybrid, reranker, { fetchTopK: 30 });\n * ```\n *\n * Reads `results: [{ index, relevance_score }]` (Cohere, Jina), `data: [{ index, relevance_score }]`\n * (Voyage) and a bare `[{ index, score }]` (TEI). Each passage keeps its fields and takes the new\n * score; the list is re-sorted and cut to `topK`. Indices the server invents are ignored.\n */\nexport class HttpReranker implements Reranker {\n private readonly format: NonNullable<HttpRerankerOptions['format']>;\n private readonly doFetch: typeof fetch;\n\n constructor(private readonly options: HttpRerankerOptions) {\n this.format = options.format ?? 'auto';\n this.doFetch = options.fetch ?? fetch;\n }\n\n async rerank(\n query: string,\n passages: Passage[],\n options: RerankOptions = {},\n ): Promise<Passage[]> {\n if (passages.length === 0) {\n return [];\n }\n const topK = options.topK ?? passages.length;\n const documents = passages.map((passage) => passage.text);\n const response = await this.doFetch(this.options.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(this.options.apiKey ? { authorization: `Bearer ${this.options.apiKey}` } : {}),\n ...this.options.headers,\n },\n body: JSON.stringify({\n ...(this.options.model !== undefined ? { model: this.options.model } : {}),\n query,\n ...(this.format !== 'tei' ? { documents, top_n: topK } : {}),\n ...(this.format !== 'cohere' ? { texts: documents } : {}),\n }),\n signal: AbortSignal.timeout(this.options.timeoutMs ?? 30_000),\n });\n const body = await response.text();\n if (!response.ok) {\n throw new HttpModelError(\n response.status,\n `Rerank request failed (${response.status}): ${errorMessageOf(body)}`,\n );\n }\n const items = itemsOf(body);\n if (items === undefined) {\n throw new HttpModelError(502, 'Rerank response has no results');\n }\n const seen = new Set<number>();\n const reranked: Passage[] = [];\n for (const item of items) {\n const index = item.index;\n const passage = typeof index === 'number' ? passages[index] : undefined;\n if (passage === undefined || seen.has(index as number)) {\n continue;\n }\n seen.add(index as number);\n reranked.push({ ...passage, score: Number(item.relevance_score ?? item.score ?? 0) });\n }\n reranked.sort((a, b) => b.score - a.score);\n return reranked.slice(0, topK);\n }\n}\n\nfunction itemsOf(body: string): RerankItem[] | undefined {\n let json: unknown;\n try {\n json = JSON.parse(body);\n } catch {\n return undefined;\n }\n if (Array.isArray(json)) {\n return json as RerankItem[];\n }\n const wrapped = json as { results?: unknown; data?: unknown } | null;\n const items = wrapped?.results ?? wrapped?.data;\n return Array.isArray(items) ? (items as RerankItem[]) : undefined;\n}\n","/**\n * The data model of tree navigation retrieval: a hierarchical table of contents per long document\n * (PageIndex-style: sections with page ranges and summaries), built once at ingestion and read by an\n * LLM at query time to walk to the relevant sections instead of ranking hundreds of look-alike chunks.\n */\n\n/**\n * One addressable piece of a document's text, in reading order — a PDF page, or a section of a\n * markdown/HTML document. Tree nodes point at ranges of units, and navigation reads the text of the\n * units a chosen node spans. `index` is the unit's zero-based position; `page` its 1-based page\n * number when the document has pages.\n */\nexport interface TreeUnit {\n index: number;\n text: string;\n /** 1-based page number (PDF). Defaults to `index + 1` for page input; absent for section input. */\n page?: number;\n}\n\n/**\n * A heading the document declares about itself: a PDF outline (bookmark) entry, a markdown header,\n * a section boundary your extractor already knows. `unit` is the zero-based unit it starts in;\n * `page` (1-based) is accepted instead for page input. `level` 1 is top level.\n */\nexport interface TreeHeading {\n title: string;\n level: number;\n unit?: number;\n page?: number;\n}\n\n/** A section as produced by an extractor that already splits on headings. */\nexport interface TreeSection {\n text: string;\n title?: string;\n /** Heading depth, 1 = top level. Sections without a level are treated as level 1. */\n level?: number;\n /** 1-based page the section starts on, when known. */\n page?: number;\n}\n\n/**\n * What {@link import('./build.js').buildDocumentTree} builds from. Give exactly one of `pages`\n * (one string per page, in order) or `sections` (heading-split text). `outline`, when present, is the\n * cheapest and most reliable structure there is (a PDF's bookmarks) and is used before any heuristic.\n */\nexport interface DocumentTreeInput {\n pages?: string[];\n sections?: TreeSection[];\n /** Headings the document declares (PDF outline/bookmarks), in reading order. */\n outline?: TreeHeading[];\n /** Document title, shown to the navigating LLM and used as the citation source. */\n title?: string;\n}\n\n/** How the structure of a tree was obtained, cheapest first. */\nexport type TreeStructureSource =\n /** From {@link DocumentTreeInput.outline}. */\n | 'outline'\n /** From section titles/levels or headings detected in the text — no LLM. */\n | 'headings'\n /** An LLM proposed the sections (the document had no usable outline). */\n | 'llm'\n /** Fallback: fixed-size page groups. Always succeeds; used when everything above failed or was skipped. */\n | 'flat';\n\n/** Where a node's summary came from. */\nexport type TreeSummarySource = 'llm' | 'text' | 'reused';\n\nexport interface DocumentTreeNode {\n /** Stable within the tree: `0001`, `0002`… in depth-first order (PageIndex's scheme). */\n id: string;\n title: string;\n /** What the section covers — what the navigating LLM reads to decide whether to go in. */\n summary?: string;\n summarySource?: TreeSummarySource;\n /** First and last unit (inclusive, zero-based) the node spans, children included. */\n unitStart: number;\n unitEnd: number;\n /** First and last page (1-based, inclusive) when the document has pages. */\n pageStart?: number;\n pageEnd?: number;\n /**\n * Titles of descendants removed to respect `maxDepth` — kept because they are routing information:\n * the pages are still reachable by reading this node, the titles tell the navigator they are here.\n */\n keyItems?: string[];\n /** Hash of the node's title and the text it spans; an incremental rebuild reuses a summary when it matches. */\n contentHash: string;\n children: DocumentTreeNode[];\n}\n\n/** What a build cost and how it ended — persisted with the tree, so an operator can see it. */\nexport interface TreeBuildStats {\n llmCalls: number;\n inputTokens: number;\n outputTokens: number;\n durationMs: number;\n /** Set when the structure step fell back (to `flat`) or summaries were cut short, and why. */\n fallbackReason?: string;\n /** True when the per-document budget or timeout ran out at some point during the build. */\n budgetExhausted: boolean;\n /** Summaries carried over unchanged from the previous tree (incremental rebuild). */\n reusedSummaries: number;\n}\n\nexport interface DocumentTree {\n documentId: string;\n /** Format version of this structure. */\n version: 1;\n title?: string;\n /** One-paragraph description of the whole document (LLM, when budget allows). */\n description?: string;\n structure: TreeStructureSource;\n /**\n * Hash of the input text and every build option that shapes the tree. Equal fingerprint ⇒ the\n * build would produce the same tree, so it is skipped (see `previous` in the build options).\n */\n fingerprint: string;\n unitCount: number;\n /** Top-level nodes. */\n nodes: DocumentTreeNode[];\n stats: TreeBuildStats;\n /**\n * Document metadata used for access filtering, exactly like chunk metadata in a vector store\n * (tenant, audience, collection…). Stores apply `filter` to it with the vector stores' semantics.\n */\n metadata?: Record<string, unknown>;\n /** Citation-facing origin (title, URL), copied onto returned passages. */\n source?: string;\n /** ISO timestamp of the build. */\n builtAt: string;\n}\n\n/** Walk a tree depth-first (pre-order). */\nexport function* walkTree(\n nodes: readonly DocumentTreeNode[],\n path: readonly DocumentTreeNode[] = [],\n): Generator<{ node: DocumentTreeNode; path: readonly DocumentTreeNode[] }> {\n for (const node of nodes) {\n yield { node, path };\n yield* walkTree(node.children, [...path, node]);\n }\n}\n\n/** Index a tree's nodes by id, each with its ancestors. */\nexport function indexTree(\n tree: Pick<DocumentTree, 'nodes'>,\n): Map<string, { node: DocumentTreeNode; path: readonly DocumentTreeNode[] }> {\n const index = new Map<string, { node: DocumentTreeNode; path: readonly DocumentTreeNode[] }>();\n for (const entry of walkTree(tree.nodes)) {\n index.set(entry.node.id, entry);\n }\n return index;\n}\n","/**\n * Deterministic structure detection — the cheap path that runs before any LLM. Finds headings in\n * page text: markdown headers, and the numbered/keyword headings long structured documents use\n * (`PART 52`, `Subpart 9.1`, `Item 7.`, `52.236-5 Material and Workmanship.`, `3.2 Scope`).\n */\n\n/** A heading found in (or declared for) the document, before nesting. */\nexport interface DetectedHeading {\n title: string;\n level: number;\n unit: number;\n /** The heading is the first text of its unit, so the previous section ends in the unit before. */\n atUnitStart: boolean;\n}\n\nconst MARKDOWN = /^(#{1,6})\\s+(.+?)\\s*#*\\s*$/;\n\n/** Keyword headings, with the level each keyword usually sits at. */\nconst KEYWORD_LEVEL: Record<string, number> = {\n part: 1,\n chapter: 1,\n title: 1,\n appendix: 1,\n annex: 1,\n exhibit: 1,\n schedule: 1,\n subpart: 2,\n article: 2,\n item: 2,\n section: 3,\n};\n\nconst KEYWORD =\n /^(PART|Part|CHAPTER|Chapter|TITLE|APPENDIX|Appendix|ANNEX|Annex|EXHIBIT|Exhibit|SCHEDULE|Schedule|SUBPART|Subpart|ARTICLE|Article|ITEM|Item|SECTION|Section)\\s+([0-9]{1,4}[A-Z]?(?:[.\\-–][0-9A-Z]{1,4}){0,3}|[IVXLC]{1,6}|[A-Z])\\b\\.?(?:\\s*[—–:\\-.]\\s*|\\s+)?(.{0,110})$/;\n\n/** `3.2 Scope`, `52.236–5 Material and Workmanship.`, `1.1.4 Definitions` — dotted numbering only. */\nconst NUMBERED = /^(\\d{1,3}(?:\\.\\d{1,4}){1,4})([-–]\\d{1,4})?\\.?\\s+([A-Z[][^\\n]{1,100})$/;\n\nfunction clean(title: string): string {\n return title\n .replace(/\\s+/g, ' ')\n .replace(/[\\s.:—–-]+$/, '')\n .trim();\n}\n\n/** Does this line look like prose or a table row rather than a heading? */\nfunction looksLikeBody(rest: string): boolean {\n return /(\\d\\)?|[%$])\\s*$/.test(rest) || rest.split(' ').length > 16;\n}\n\n/** Words a heading title rarely ends with — a line ending in one is wrapped body text. */\nconst DANGLING = new Set(\n 'a an the of to in for and or as by with on at from that which is are be than under'.split(' '),\n);\n\nfunction dangling(title: string): boolean {\n if (/[,;(]$/.test(title)) {\n return true;\n }\n const last = title.split(/\\s+/).pop()?.toLowerCase() ?? '';\n return DANGLING.has(last);\n}\n\n/**\n * Detect headings in each unit's text. `markdownOnly` restricts detection to markdown headers (for\n * text you know is markdown, where a numbered line is more likely a list item).\n *\n * Besides running headers (below), it drops **table-of-contents listings**: five or more\n * consecutive keyword/numbered heading lines with no body text between them are a contents page\n * (a regulation part opens with one), not sections — only a leading top-level heading of the run\n * (`PART 2—…`) and its last line (the first real heading, which body text follows) are kept. Markdown headers are explicit and never dropped this way.\n */\nexport function detectHeadings(texts: readonly string[], markdownOnly = false): DetectedHeading[] {\n const headings: DetectedHeading[] = [];\n let inFence = false;\n /** The current run of consecutive non-markdown heading lines, as indexes into `headings`. */\n let run: { index: number; keyword: boolean }[] = [];\n const dropped = new Set<number>();\n const closeRun = () => {\n if (run.length >= 5) {\n run.forEach((entry, position) => {\n const heading = headings[entry.index];\n // The last line of the run is followed by body text: that one is the real heading the\n // contents page was listing ahead of.\n const keep =\n position === run.length - 1 ||\n (position === 0 && entry.keyword && heading !== undefined && heading.level === 1);\n if (!keep) {\n dropped.add(entry.index);\n }\n });\n }\n run = [];\n };\n texts.forEach((text, unit) => {\n let seenText = false;\n const lines = text.split('\\n').map((raw) => raw.trim());\n for (let at = 0; at < lines.length; at++) {\n let line = lines[at] ?? '';\n if (line.startsWith('```')) {\n inFence = !inFence;\n seenText = true;\n closeRun();\n continue;\n }\n if (line === '') {\n continue;\n }\n if (inFence || line.length > 140) {\n seenText = true;\n closeRun();\n continue;\n }\n const atUnitStart = !seenText;\n seenText = true;\n const markdown = MARKDOWN.exec(line);\n if (markdown?.[1] !== undefined && markdown[2] !== undefined) {\n closeRun();\n const title = clean(markdown[2].replace(/\\*\\*/g, ''));\n if (title !== '') {\n headings.push({ title, level: markdown[1].length, unit, atUnitStart });\n }\n continue;\n }\n if (markdownOnly) {\n continue;\n }\n const candidate = KEYWORD.test(line) || NUMBERED.test(line);\n if (candidate) {\n // A heading hyphenated over lines (`52.236–3 Site Investigation and Condi-` / `tions\n // Affecting the Work.`): join up to two continuation lines.\n for (let joined = 0; joined < 2; joined++) {\n const next = lines[at + 1];\n if (!/[a-z]-$/.test(line) || next === undefined || next === '' || next.length > 80) {\n break;\n }\n line = `${line.slice(0, -1)}${next}`;\n at += 1;\n }\n }\n const keyword = KEYWORD.exec(line);\n if (keyword?.[1] !== undefined && keyword[2] !== undefined) {\n const rest = keyword[3] ?? '';\n const allCaps = keyword[1] === keyword[1].toUpperCase();\n if (\n !looksLikeBody(rest) &&\n !/^[a-z,;)]/.test(rest) &&\n (rest !== '' || allCaps) &&\n !dangling(clean(line))\n ) {\n const level = KEYWORD_LEVEL[keyword[1].toLowerCase()] ?? 2;\n run.push({ index: headings.length, keyword: true });\n headings.push({ title: clean(line), level, unit, atUnitStart });\n continue;\n }\n }\n const numbered = NUMBERED.exec(line);\n if (\n numbered?.[1] !== undefined &&\n numbered[3] !== undefined &&\n !looksLikeBody(numbered[3]) &&\n !dangling(clean(numbered[3]))\n ) {\n // Below the keyword levels: `52.236` is 3, `52.236–5` is 4, `1.1.4` is 4.\n const level = Math.min(6, numbered[1].split('.').length + 1 + (numbered[2] ? 1 : 0));\n run.push({ index: headings.length, keyword: false });\n headings.push({ title: clean(line), level, unit, atUnitStart });\n continue;\n }\n // A body line ends a run of heading lines; a heading-shaped line that was rejected (a contents\n // entry cut mid-title), a short Title Case line (its wrapped second line) or page furniture\n // does not.\n if (!candidate && /[A-Za-z]{3,}/.test(line) && !isPageFurniture(line) && !isTitleCase(line)) {\n closeRun();\n }\n }\n });\n closeRun();\n return dropContentsDuplicates(\n dropRunningHeaders(\n headings.filter((_, index) => !dropped.has(index)),\n texts.length,\n ),\n );\n}\n\n/**\n * A numbered title (`19.800 General`, `Subpart 19.8—…`) names one section; seen twice, the earlier\n * one is a contents entry pointing at the later — keep the later. Titles without a number\n * (\"General\", \"Definitions\") legitimately repeat and are all kept.\n */\nfunction dropContentsDuplicates(headings: DetectedHeading[]): DetectedHeading[] {\n const last = new Map<string, number>();\n headings.forEach((heading, index) => {\n if (/\\d/.test(heading.title)) {\n last.set(heading.title.toLowerCase(), index);\n }\n });\n return headings.filter((heading, index) => {\n const key = heading.title.toLowerCase();\n return !last.has(key) || last.get(key) === index;\n });\n}\n\n/** A short line whose longer words are mostly capitalized — a title fragment, not prose. */\nfunction isTitleCase(line: string): boolean {\n if (line.length > 70 || /\\.\\s*$/.test(line)) {\n return false;\n }\n const words = line.split(/\\s+/).filter((word) => /^[A-Za-z]{4,}/.test(word));\n return (\n words.length > 0 && words.filter((word) => /^[A-Z]/.test(word)).length / words.length >= 0.6\n );\n}\n\n/** A page number, a running citation line (`48 CFR Ch. 1 (10–1–24 Edition)`) — neither body nor heading. */\nfunction isPageFurniture(line: string): boolean {\n return (\n line.length < 50 &&\n /^[\\d\\s\\-–—.()]*(\\d+\\s*CFR|Federal Acquisition Regulation|Page \\d+)/i.test(line)\n );\n}\n\n/**\n * Lines that repeat across many units (running headers/footers, page furniture), digits ignored so\n * `Page 3 of 40` and `Page 4 of 40` count as one. Used to pick meaningful lines for group titles.\n */\nexport function runningLines(texts: readonly string[]): Set<string> {\n const counts = new Map<string, number>();\n for (const text of texts) {\n const seen = new Set<string>();\n for (const raw of text.split('\\n')) {\n const key = raw.trim().replace(/\\d+/g, '#').toLowerCase();\n if (key && !seen.has(key)) {\n seen.add(key);\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n }\n }\n const threshold = Math.max(3, Math.ceil(texts.length * 0.2));\n return new Set([...counts].filter(([, count]) => count >= threshold).map(([key]) => key));\n}\n\n/**\n * Remove running headers/footers: the same title repeated on many units (\"PART 52—SOLICITATION\n * PROVISIONS\" printed atop every page) is page furniture, not structure. Keeps the first occurrence.\n */\nfunction dropRunningHeaders(headings: DetectedHeading[], unitCount: number): DetectedHeading[] {\n const counts = new Map<string, number>();\n for (const heading of headings) {\n const key = heading.title.toLowerCase();\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n const seen = new Set<string>();\n const threshold = Math.max(3, Math.ceil(unitCount * 0.2));\n return headings.filter((heading) => {\n const key = heading.title.toLowerCase();\n if ((counts.get(key) ?? 0) >= threshold) {\n if (seen.has(key)) {\n return false;\n }\n }\n seen.add(key);\n return true;\n });\n}\n","import type { ModelProvider, SinkWriter } from '@dudousxd/nestjs-agent-core';\nimport { HttpModelError, errorMessageOf } from '../openai-embeddings.js';\n\n/**\n * The structured side of every tree-LLM request: what is being asked, as data. The prompt says the\n * same thing in prose; `task` exists so a deterministic fake (tests, offline dev — see\n * {@link import('./keyword-tree-llm.js').keywordTreeLlm}) can answer without parsing prose, and so a\n * host can log or route requests by kind.\n */\nexport type TreeLlmTask =\n | {\n kind: 'structure';\n /** The window of pages the LLM is asked to find section starts in. */\n units: { unit: number; page?: number; text: string }[];\n }\n | { kind: 'summarize'; title: string; text: string }\n | { kind: 'describe'; title?: string; outline: { title: string; summary?: string }[] }\n | {\n kind: 'navigate';\n query: string;\n /** `single-pass`: the whole tree is shown; `beam`: one level of candidates at a time. */\n mode: 'single-pass' | 'beam';\n candidates: TreeLlmCandidate[];\n /** Max nodes the answer may name. */\n maxNodes: number;\n }\n | {\n kind: 'select';\n query: string;\n documents: { id: string; title?: string; description?: string }[];\n maxDocuments: number;\n };\n\n/** A node as shown to the navigating LLM. */\nexport interface TreeLlmCandidate {\n id: string;\n title: string;\n summary?: string;\n pages?: string;\n /** Present on beam candidates: whether the node can be expanded. */\n hasChildren?: boolean;\n /** Present in single-pass mode: depth (0 = top level). */\n depth?: number;\n}\n\nexport interface TreeLlmRequest {\n task: TreeLlmTask;\n system: string;\n prompt: string;\n /** A cap for the reply. Adapters pass it as `max_tokens` when they can. */\n maxOutputTokens: number;\n /** Aborted when the call's timeout or the build/navigation deadline passes. */\n signal: AbortSignal;\n}\n\nexport interface TreeLlmResponse {\n text: string;\n /** Provider-reported usage. When absent, the budget estimates ~4 characters per token. */\n usage?: { inputTokens?: number; outputTokens?: number };\n}\n\n/**\n * The only LLM surface tree building and navigation need: one prompt in, one text reply out. Replies\n * are expected to be JSON (for structure / navigate / select) or plain text (summaries); parsing is\n * lenient (code fences, prose around the object). Bring any model:\n * {@link treeLlmFromModelProvider} (the agent's own `ModelProvider`), {@link openAiChatTreeLlm}\n * (any OpenAI-compatible `/chat/completions`), or a function of your own. Use a deterministic\n * setting (temperature 0) — trees are cached by content, so a flaky model makes rebuilds noisy.\n */\nexport type TreeLlm = (request: TreeLlmRequest) => Promise<TreeLlmResponse>;\n\n/**\n * Hard limits for one document build or one navigation. Every LLM call is checked against them\n * **before** it is made (with the prompt's estimated size), so a budget is never overshot by more\n * than one reply, and nothing retries past it: the failure mode this exists for is an indexer that\n * burned ~1,240 calls on one PDF it could not structure.\n */\nexport interface TreeBudget {\n /** Max LLM calls. */\n maxCalls?: number;\n /** Max input (prompt) tokens across all calls, estimated at ~4 chars/token before each call. */\n maxInputTokens?: number;\n /** Max output tokens across all calls. */\n maxOutputTokens?: number;\n /** Wall-clock deadline for the whole operation, ms. */\n timeoutMs?: number;\n /** Per-call timeout, ms. Default: whatever is left of `timeoutMs`. */\n callTimeoutMs?: number;\n}\n\n/** Thrown internally when a budget or deadline would be exceeded; callers turn it into a fallback. */\nexport class TreeBudgetExceededError extends Error {\n constructor(readonly limit: 'calls' | 'inputTokens' | 'outputTokens' | 'timeout') {\n super(`tree LLM budget exhausted (${limit})`);\n this.name = 'TreeBudgetExceededError';\n }\n}\n\n/** ~4 characters per token: the budget's estimate when a provider reports no usage. */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\n/** Tracks spend against a {@link TreeBudget} and runs calls under it. Not exported from the package. */\nexport class BudgetedLlm {\n calls = 0;\n inputTokens = 0;\n outputTokens = 0;\n exhausted = false;\n private readonly deadline: number;\n\n constructor(\n private readonly llm: TreeLlm,\n private readonly budget: Required<\n Pick<TreeBudget, 'maxCalls' | 'maxInputTokens' | 'maxOutputTokens'>\n > &\n TreeBudget,\n private readonly outerSignal?: AbortSignal,\n ) {\n this.deadline =\n budget.timeoutMs !== undefined ? Date.now() + budget.timeoutMs : Number.POSITIVE_INFINITY;\n }\n\n /** Time left before the deadline, ms (`Infinity` without one). */\n remainingMs(): number {\n return this.deadline - Date.now();\n }\n\n /** Would a call with this prompt fit? Does not reserve anything; a `false` marks the budget exhausted. */\n fits(prompt: string, maxOutputTokens: number): boolean {\n const fits = this.limitHit(prompt, maxOutputTokens) === undefined;\n if (!fits) {\n this.exhausted = true;\n }\n return fits;\n }\n\n private limitHit(\n prompt: string,\n maxOutputTokens: number,\n ): TreeBudgetExceededError['limit'] | undefined {\n if (this.calls >= this.budget.maxCalls) {\n return 'calls';\n }\n if (this.inputTokens + estimateTokens(prompt) > this.budget.maxInputTokens) {\n return 'inputTokens';\n }\n // A reply may be shorter than its cap; only refuse when nothing at all is left.\n if (this.outputTokens + Math.min(maxOutputTokens, 16) > this.budget.maxOutputTokens) {\n return 'outputTokens';\n }\n if (this.remainingMs() <= 0 || this.outerSignal?.aborted) {\n return 'timeout';\n }\n return undefined;\n }\n\n /**\n * Make one call, or throw {@link TreeBudgetExceededError} without calling. Provider errors\n * propagate (after being counted). Never retries.\n */\n async call(\n task: TreeLlmTask,\n system: string,\n prompt: string,\n maxOutputTokens: number,\n ): Promise<string> {\n const limit = this.limitHit(system + prompt, maxOutputTokens);\n if (limit !== undefined) {\n this.exhausted = true;\n throw new TreeBudgetExceededError(limit);\n }\n const outputCap = Math.max(\n 16,\n Math.min(maxOutputTokens, this.budget.maxOutputTokens - this.outputTokens),\n );\n const timeoutMs = Math.min(\n this.budget.callTimeoutMs ?? Number.POSITIVE_INFINITY,\n this.remainingMs(),\n );\n // Reserve the estimate now so concurrent calls cannot all pass the check on the same headroom;\n // it is corrected to the reported usage when the reply arrives.\n const reservedInput = estimateTokens(system + prompt);\n this.inputTokens += reservedInput;\n this.outputTokens += outputCap;\n const controller = new AbortController();\n const onOuterAbort = () => controller.abort(this.outerSignal?.reason);\n this.outerSignal?.addEventListener('abort', onOuterAbort, { once: true });\n let timer: ReturnType<typeof setTimeout> | undefined;\n this.calls += 1;\n try {\n const response = await Promise.race([\n this.llm({ task, system, prompt, maxOutputTokens: outputCap, signal: controller.signal }),\n new Promise<never>((_, reject) => {\n if (Number.isFinite(timeoutMs)) {\n timer = setTimeout(\n () => {\n controller.abort(new TreeBudgetExceededError('timeout'));\n reject(new TreeBudgetExceededError('timeout'));\n },\n Math.max(0, timeoutMs),\n );\n }\n }),\n ]);\n this.inputTokens += (response.usage?.inputTokens ?? reservedInput) - reservedInput;\n this.outputTokens +=\n (response.usage?.outputTokens ?? estimateTokens(response.text)) - outputCap;\n return response.text;\n } catch (error) {\n // A failed call keeps its input estimate (the provider may have billed it) but no output.\n this.outputTokens -= outputCap;\n if (error instanceof TreeBudgetExceededError) {\n this.exhausted = true;\n }\n throw error;\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n this.outerSignal?.removeEventListener('abort', onOuterAbort);\n }\n }\n}\n\n/**\n * Pull the JSON object out of a model reply: bare, fenced in ```json, or surrounded by prose.\n * Returns `undefined` when there is none (never throws).\n */\nexport function parseJsonReply(text: string): Record<string, unknown> | undefined {\n const tryParse = (candidate: string): Record<string, unknown> | undefined => {\n try {\n const value = JSON.parse(candidate) as unknown;\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n };\n const trimmed = text.trim();\n const direct = tryParse(trimmed);\n if (direct !== undefined) {\n return direct;\n }\n const fenced = /```(?:json)?\\s*([\\s\\S]*?)```/i.exec(trimmed);\n if (fenced?.[1] !== undefined) {\n const inner = tryParse(fenced[1].trim());\n if (inner !== undefined) {\n return inner;\n }\n }\n const start = trimmed.indexOf('{');\n const end = trimmed.lastIndexOf('}');\n if (start >= 0 && end > start) {\n return tryParse(trimmed.slice(start, end + 1));\n }\n return undefined;\n}\n\n/** The strings in a JSON array value (numbers stringified); anything else yields `[]`. */\nexport function stringList(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value\n .map((item) => (typeof item === 'number' ? String(item) : item))\n .filter((item): item is string => typeof item === 'string');\n}\n\nconst NOOP_SINK: SinkWriter = {\n write() {},\n end() {},\n fail() {},\n};\n\n/**\n * A {@link TreeLlm} over the agent's own {@link ModelProvider}: one `runTurn` with no tools and a\n * discarding sink. Usage comes from the provider's report.\n */\nexport function treeLlmFromModelProvider(provider: ModelProvider): TreeLlm {\n return async (request) => {\n const result = await provider.runTurn({\n system: request.system,\n messages: [{ role: 'user', content: request.prompt }],\n tools: [],\n sink: NOOP_SINK,\n abortSignal: request.signal,\n });\n return {\n text: result.text,\n usage: { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens },\n };\n };\n}\n\nexport interface OpenAiChatTreeLlmOptions {\n model: string;\n /** Base URL up to and including `/v1`. Default `https://api.openai.com/v1`. */\n baseUrl?: string;\n apiKey?: string;\n headers?: Record<string, string>;\n /** Default 0 — trees are cached by content, determinism keeps rebuilds quiet. */\n temperature?: number;\n /**\n * Send `response_format: { type: 'json_object' }` on JSON tasks. Default `false`: not every\n * OpenAI-compatible server accepts it, and replies are parsed leniently anyway.\n */\n jsonMode?: boolean;\n /** Extra body fields (a reasoning switch, provider routing). */\n body?: Record<string, unknown>;\n fetch?: typeof fetch;\n}\n\n/**\n * A {@link TreeLlm} over any OpenAI-compatible `POST /v1/chat/completions` — OpenAI, a gateway,\n * vLLM, Ollama, DeepSeek, Qwen… — with no SDK dependency. Throws `HttpModelError` on a non-2xx.\n */\nexport function openAiChatTreeLlm(options: OpenAiChatTreeLlmOptions): TreeLlm {\n const url = `${(options.baseUrl ?? 'https://api.openai.com/v1').replace(/\\/+$/, '')}/chat/completions`;\n const doFetch = options.fetch ?? fetch;\n return async (request) => {\n const json = request.task.kind !== 'summarize';\n const response = await doFetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n messages: [\n { role: 'system', content: request.system },\n { role: 'user', content: request.prompt },\n ],\n temperature: options.temperature ?? 0,\n max_tokens: request.maxOutputTokens,\n ...(options.jsonMode && json ? { response_format: { type: 'json_object' } } : {}),\n ...options.body,\n }),\n signal: request.signal,\n });\n const body = await response.text();\n if (!response.ok) {\n throw new HttpModelError(\n response.status,\n `Chat completion failed (${response.status}): ${errorMessageOf(body)}`,\n );\n }\n const parsed = parseJsonReply(body) as\n | {\n choices?: { message?: { content?: unknown } }[];\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n }\n | undefined;\n const content = parsed?.choices?.[0]?.message?.content;\n if (typeof content !== 'string') {\n throw new HttpModelError(502, 'Chat completion returned no message content');\n }\n const usage: NonNullable<TreeLlmResponse['usage']> = {};\n if (typeof parsed?.usage?.prompt_tokens === 'number') {\n usage.inputTokens = parsed.usage.prompt_tokens;\n }\n if (typeof parsed?.usage?.completion_tokens === 'number') {\n usage.outputTokens = parsed.usage.completion_tokens;\n }\n return { text: content, usage };\n };\n}\n\n/** A get/set cache — a `Map`, an LRU, a Redis wrapper. Values are whole replies. */\nexport interface TreeLlmCache {\n get(key: string): TreeLlmResponse | undefined | Promise<TreeLlmResponse | undefined>;\n set(key: string, value: TreeLlmResponse): unknown;\n}\n\n/**\n * Memoize a {@link TreeLlm} by the exact request (task kind, system prompt, prompt, output cap).\n * With a deterministic model that makes a rebuild of an unchanged section, or a repeated question,\n * free. Cached replies report zero usage, so they cost nothing against a budget but a call slot.\n */\nexport function cachedTreeLlm(llm: TreeLlm, cache: TreeLlmCache = new Map()): TreeLlm {\n return async (request) => {\n const key = stableHash(\n `${request.task.kind}\\u0000${request.maxOutputTokens}\\u0000${request.system}\\u0000${request.prompt}`,\n );\n const hit = await cache.get(key);\n if (hit !== undefined) {\n return { text: hit.text, usage: { inputTokens: 0, outputTokens: 0 } };\n }\n const response = await llm(request);\n await cache.set(key, response);\n return response;\n };\n}\n\n/**\n * A fast, deterministic, non-cryptographic 64-bit hash (two independent 32-bit FNV-1a/murmur-mixed\n * lanes) as 16 hex chars — content fingerprints and cache keys, not security.\n */\nexport function stableHash(text: string): string {\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193 ^ text.length;\n for (let i = 0; i < text.length; i++) {\n const c = text.charCodeAt(i);\n h1 = Math.imul(h1 ^ c, 0x01000193);\n h2 = Math.imul(h2 ^ c, 0x5bd1e995);\n h2 ^= h2 >>> 13;\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 0x85ebca6b);\n h2 = Math.imul(h2 ^ (h2 >>> 15), 0xc2b2ae35);\n return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0');\n}\n","import { type DetectedHeading, detectHeadings, runningLines } from './headings.js';\nimport {\n BudgetedLlm,\n type TreeBudget,\n TreeBudgetExceededError,\n type TreeLlm,\n parseJsonReply,\n stableHash,\n} from './llm.js';\nimport type {\n DocumentTree,\n DocumentTreeInput,\n DocumentTreeNode,\n TreeStructureSource,\n TreeUnit,\n} from './types.js';\nimport { walkTree } from './types.js';\n\nexport interface BuildDocumentTreeOptions {\n documentId: string;\n /**\n * The model for summaries, the document description, and structuring documents with no usable\n * outline. Omit it and the build is LLM-free: outline/headings or page groups, with the start of\n * each section's text as its summary.\n */\n llm?: TreeLlm;\n /** Deepest level kept (1 = top-level sections only). Deeper headings become `keyItems`. Default 4. */\n maxDepth?: number;\n /**\n * Hard per-document limits for every LLM call of the build. Defaults: 60 calls, 400k input\n * tokens, 40k output tokens, 5 minutes overall, 60 s per call. Running out is not an error: the\n * structure step falls back to page groups, and remaining summaries use the section's own text.\n */\n budget?: TreeBudget;\n /** Summarize nodes with the LLM. Default `true` when `llm` is set. */\n summaries?: boolean;\n /** A node whose text is shorter than this is its own summary (no LLM call). Default 600 chars. */\n summaryMinChars?: number;\n /** Text of a node sent to the summarizer, at most. Default 8 000 chars. */\n summaryInputChars?: number;\n /** Concurrent summary calls. Default 4. */\n concurrency?: number;\n /** Write a one-paragraph description of the document. Default `true` when `llm` is set. */\n describe?: boolean;\n /**\n * Find headings in the text (markdown headers, `PART 52`, `Item 7.`, `3.2 Scope`…) when there is\n * no outline and sections carry no titles. `'markdown'` only trusts markdown headers. Default `true`.\n */\n detectHeadings?: boolean | 'markdown';\n /** At least this many headings for a detected structure to be used. Default 3. */\n minHeadings?: number;\n /** Ask the LLM for the structure when nothing deterministic worked. Default `true` when `llm` is set. */\n llmStructure?: boolean;\n /** Characters of text per structuring call. Default 24 000. */\n structureWindowChars?: number;\n /**\n * Structuring calls allowed. A document needing more windows than this is not sent at all —\n * it goes straight to page groups instead of spending a budget it cannot finish. Default 12.\n */\n maxStructureCalls?: number;\n /** Units per page-group node (flat trees, and splitting long leaves). Default 5. */\n groupSize?: number;\n /** A leaf spanning more units than this is split into page groups. Default 10. */\n maxLeafUnits?: number;\n /** Children per level before page groups are grouped again (flat trees). Default 12. */\n maxChildren?: number;\n /** The previous tree of this document: an unchanged input returns it as is; changed nodes are re-summarized, the rest reuse their summaries. */\n previous?: DocumentTree;\n metadata?: Record<string, unknown>;\n source?: string;\n signal?: AbortSignal;\n}\n\nexport interface BuiltDocumentTree {\n tree: DocumentTree;\n /** The units the node ranges index — persist them with the tree ({@link import('./store.js').DocumentTreeStore.put}). */\n units: TreeUnit[];\n /** `true` when `previous` had the same fingerprint and was returned without building. */\n unchanged: boolean;\n}\n\nconst DEFAULT_BUDGET = {\n maxCalls: 60,\n maxInputTokens: 400_000,\n maxOutputTokens: 40_000,\n timeoutMs: 300_000,\n callTimeoutMs: 60_000,\n} as const;\n\nconst STRUCTURE_SYSTEM =\n 'You extract the table of contents of a document. You answer with JSON only, no prose.';\nconst SUMMARY_SYSTEM =\n 'You summarize sections of a document so a reader can decide whether a section answers their question. Be specific: name the topics, clauses, figures and defined terms it covers.';\n\n/** Turn the input into units (pages or sections) plus any headings it declares. */\nexport function toUnits(input: DocumentTreeInput): {\n units: TreeUnit[];\n declared: DetectedHeading[] | undefined;\n kind: 'pages' | 'sections';\n} {\n if (input.pages !== undefined && input.sections !== undefined) {\n throw new Error('buildDocumentTree: give `pages` or `sections`, not both');\n }\n if (input.sections !== undefined) {\n const units = input.sections.map((section, index) => ({\n index,\n text: section.text,\n ...(section.page !== undefined ? { page: section.page } : {}),\n }));\n const titled = input.sections.some((section) => section.title?.trim());\n const declared = titled\n ? input.sections.flatMap((section, unit) =>\n section.title?.trim()\n ? [\n {\n title: section.title.trim(),\n level: Math.max(1, Math.min(6, section.level ?? 1)),\n unit,\n atUnitStart: true,\n },\n ]\n : [],\n )\n : undefined;\n return { units, declared, kind: 'sections' };\n }\n const pages = input.pages ?? [];\n return {\n units: pages.map((text, index) => ({ index, text, page: index + 1 })),\n declared: undefined,\n kind: 'pages',\n };\n}\n\nfunction outlineHeadings(input: DocumentTreeInput, units: TreeUnit[]): DetectedHeading[] {\n const headings: DetectedHeading[] = [];\n for (const entry of input.outline ?? []) {\n let unit = entry.unit;\n if (unit === undefined && entry.page !== undefined) {\n unit = units.findIndex((candidate) => candidate.page === entry.page);\n if (unit < 0) {\n unit = entry.page - 1;\n }\n }\n if (unit === undefined || unit < 0 || unit >= units.length || !entry.title.trim()) {\n continue;\n }\n headings.push({\n title: entry.title.trim(),\n level: Math.max(1, Math.min(6, Math.floor(entry.level))),\n unit,\n atUnitStart: false,\n });\n }\n // Outlines are usually ordered, but some PDFs list bookmarks out of page order.\n return headings.sort((a, b) => a.unit - b.unit);\n}\n\ninterface DraftNode {\n title: string;\n unitStart: number;\n unitEnd: number;\n keyItems?: string[];\n children: DraftNode[];\n}\n\n/** Nest headings by level (a stack, like PageIndex's markdown tree) and compute each span. */\nfunction nestHeadings(headings: DetectedHeading[], unitCount: number): DraftNode[] {\n const roots: DraftNode[] = [];\n const stack: { node: DraftNode; level: number }[] = [];\n headings.forEach((heading, index) => {\n // The node ends where the next heading of the same or a higher level starts.\n let end = unitCount - 1;\n for (let next = index + 1; next < headings.length; next++) {\n const candidate = headings[next];\n if (candidate !== undefined && candidate.level <= heading.level) {\n end = candidate.atUnitStart ? candidate.unit - 1 : candidate.unit;\n break;\n }\n }\n const node: DraftNode = {\n title: heading.title,\n unitStart: heading.unit,\n unitEnd: Math.max(heading.unit, end),\n children: [],\n };\n while (stack.length > 0 && (stack[stack.length - 1]?.level ?? 0) >= heading.level) {\n stack.pop();\n }\n const parent = stack[stack.length - 1];\n if (parent === undefined) {\n roots.push(node);\n } else {\n parent.node.children.push(node);\n parent.node.unitEnd = Math.max(parent.node.unitEnd, node.unitEnd);\n }\n stack.push({ node, level: heading.level });\n });\n // Front matter before the first heading stays reachable.\n const first = roots[0];\n if (first !== undefined && first.unitStart > 0) {\n roots.unshift({\n title: 'Front matter',\n unitStart: 0,\n unitEnd: first.unitStart - 1,\n children: [],\n });\n }\n return roots;\n}\n\n/** The first meaningful line of a unit: not page furniture (a running header, a page number). */\nfunction firstLine(text: string, running: ReadonlySet<string>): string {\n const line = text\n .split('\\n')\n .map((candidate) => candidate.trim())\n .find(\n (candidate) =>\n /[A-Za-z]{3,}/.test(candidate) &&\n !running.has(candidate.replace(/\\d+/g, '#').toLowerCase()),\n );\n return line === undefined ? '' : line.length > 80 ? `${line.slice(0, 77)}…` : line;\n}\n\nfunction rangeLabel(units: TreeUnit[], start: number, end: number): string {\n const first = units[start];\n const last = units[end];\n if (first?.page !== undefined && last?.page !== undefined) {\n return first.page === last.page ? `Page ${first.page}` : `Pages ${first.page}–${last.page}`;\n }\n return start === end ? `Section ${start + 1}` : `Sections ${start + 1}–${end + 1}`;\n}\n\n/** Fixed-size groups over `[start, end]`, grouped again while a level is wider than `maxChildren`. */\nfunction groupNodes(\n units: TreeUnit[],\n start: number,\n end: number,\n groupSize: number,\n maxChildren: number,\n levelsLeft: number,\n running: ReadonlySet<string>,\n): DraftNode[] {\n let level: DraftNode[] = [];\n for (let from = start; from <= end; from += groupSize) {\n const to = Math.min(end, from + groupSize - 1);\n const lead = firstLine(units[from]?.text ?? '', running);\n level.push({\n title: `${rangeLabel(units, from, to)}${lead ? `: ${lead}` : ''}`,\n unitStart: from,\n unitEnd: to,\n children: [],\n });\n }\n let depth = 1;\n while (level.length > maxChildren && depth < levelsLeft) {\n const parents: DraftNode[] = [];\n const per = Math.max(maxChildren, Math.ceil(level.length / maxChildren));\n for (let index = 0; index < level.length; index += per) {\n const children = level.slice(index, index + per);\n const from = children[0]?.unitStart ?? start;\n const to = children[children.length - 1]?.unitEnd ?? end;\n parents.push({ title: rangeLabel(units, from, to), unitStart: from, unitEnd: to, children });\n }\n level = parents;\n depth += 1;\n }\n return level;\n}\n\n/** Titles of every descendant, at most 30 — routing information kept when the nodes go. */\nfunction descendantTitles(nodes: DraftNode[], into: string[] = []): string[] {\n for (const { node } of walkDraft(nodes)) {\n if (into.length < 30) {\n into.push(node.title);\n }\n }\n return into;\n}\n\n/**\n * Enforce `maxDepth` (deeper titles become `keyItems`), split long leaves into page groups, and\n * collapse what retrieval could not tell apart anyway — retrieval reads whole units, so:\n *\n * - a lone child spanning exactly its parent is folded into it;\n * - a node spanning a single unit keeps its descendants only as `keyItems`;\n * - consecutive leaf siblings spanning the same units merge into the first (the others' titles\n * become its `keyItems`), instead of each getting its own — identical — summary call.\n */\nfunction shape(\n nodes: DraftNode[],\n units: TreeUnit[],\n depth: number,\n options: {\n maxDepth: number;\n maxLeafUnits: number;\n groupSize: number;\n maxChildren: number;\n running: ReadonlySet<string>;\n },\n): DraftNode[] {\n const shaped = nodes.map((node) => {\n if (\n depth >= options.maxDepth ||\n (node.unitStart === node.unitEnd && node.children.length > 0)\n ) {\n const keyItems = descendantTitles(node.children, [...(node.keyItems ?? [])]);\n return {\n ...node,\n ...(keyItems.length > 0 ? { keyItems } : {}),\n children: [],\n };\n }\n let children = node.children;\n const only = children[0];\n if (\n children.length === 1 &&\n only !== undefined &&\n only.unitStart === node.unitStart &&\n only.unitEnd === node.unitEnd\n ) {\n children = only.children;\n }\n if (children.length === 0 && node.unitEnd - node.unitStart + 1 > options.maxLeafUnits) {\n children = groupNodes(\n units,\n node.unitStart,\n node.unitEnd,\n options.groupSize,\n options.maxChildren,\n options.maxDepth - depth,\n options.running,\n );\n }\n return { ...node, children: shape(children, units, depth + 1, options) };\n });\n const merged: DraftNode[] = [];\n for (const node of shaped) {\n const previous = merged[merged.length - 1];\n if (\n previous !== undefined &&\n previous.children.length === 0 &&\n node.children.length === 0 &&\n previous.unitStart === node.unitStart &&\n previous.unitEnd === node.unitEnd\n ) {\n previous.keyItems = [\n ...(previous.keyItems ?? []),\n node.title,\n ...(node.keyItems ?? []),\n ].slice(0, 30);\n continue;\n }\n merged.push(node);\n }\n return merged;\n}\n\nfunction* walkDraft(nodes: DraftNode[]): Generator<{ node: DraftNode }> {\n for (const node of nodes) {\n yield { node };\n yield* walkDraft(node.children);\n }\n}\n\n/** The text a node spans (children included), joined in reading order. */\nexport function spanText(units: readonly TreeUnit[], start: number, end: number): string {\n const parts: string[] = [];\n for (let index = Math.max(0, start); index <= end && index < units.length; index++) {\n parts.push(units[index]?.text ?? '');\n }\n return parts.join('\\n\\n');\n}\n\n/**\n * The text of a node before its first child (its own introduction), or its whole span for a leaf —\n * one string per unit, the first starting at the node's heading when it can be found in the unit\n * (a heading in the middle of a page should not be summarized from the top of that page).\n */\nfunction ownTexts(units: readonly TreeUnit[], node: DocumentTreeNode): string[] {\n const first = node.children[0];\n const end = first === undefined ? node.unitEnd : Math.max(node.unitStart, first.unitStart - 1);\n const texts: string[] = [];\n for (let index = node.unitStart; index <= end && index < units.length; index++) {\n texts.push(units[index]?.text ?? '');\n }\n const head = texts[0];\n if (head !== undefined) {\n const probe = node.title.slice(0, 30);\n const at = probe.length >= 4 ? head.indexOf(probe) : -1;\n if (at > 0) {\n texts[0] = head.slice(at);\n }\n }\n return texts;\n}\n\n/**\n * At most `max` characters of `texts`, spread evenly across them — a node spanning ten pages is\n * summarized from all ten, not from the first two.\n */\nfunction excerptOf(texts: readonly string[], max: number): string {\n const total = texts.reduce((sum, text) => sum + text.length, 0);\n if (total <= max) {\n return texts.join('\\n\\n');\n }\n const per = Math.max(400, Math.floor(max / texts.length));\n const parts: string[] = [];\n let used = 0;\n for (const text of texts) {\n if (used >= max) {\n break;\n }\n const part = text.length > per ? `${text.slice(0, per)}…` : text;\n parts.push(part);\n used += part.length;\n }\n return parts.join('\\n\\n');\n}\n\nfunction collapse(text: string, max: number): string {\n const flat = text.replace(/\\s+/g, ' ').trim();\n return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;\n}\n\n/** Ask the LLM for section starts, window by window. Throws on any failure (caller falls back). */\nasync function llmHeadings(\n llm: BudgetedLlm,\n units: TreeUnit[],\n options: { windowChars: number; maxCalls: number },\n): Promise<DetectedHeading[]> {\n const perUnit = Math.max(500, Math.floor(options.windowChars / 4));\n const windows: TreeUnit[][] = [];\n let current: TreeUnit[] = [];\n let size = 0;\n for (const unit of units) {\n const length = Math.min(unit.text.length, perUnit);\n if (current.length > 0 && size + length > options.windowChars) {\n windows.push(current);\n current = [];\n size = 0;\n }\n current.push(unit);\n size += length;\n }\n if (current.length > 0) {\n windows.push(current);\n }\n if (windows.length > options.maxCalls) {\n throw new Error(\n `document needs ${windows.length} structuring calls, more than maxStructureCalls (${options.maxCalls})`,\n );\n }\n const headings: DetectedHeading[] = [];\n for (const window of windows) {\n const shown = window.map((unit) => ({\n unit: unit.index,\n ...(unit.page !== undefined ? { page: unit.page } : {}),\n text: unit.text.length > perUnit ? `${unit.text.slice(0, perUnit)}…` : unit.text,\n }));\n const body = shown\n .map((unit) => `<unit ${unit.unit}>\\n${unit.text}\\n</unit ${unit.unit}>`)\n .join('\\n');\n const prompt = `Below is part of a document, split into numbered units (pages or sections).\nList the headings of the sections that START in these units, in reading order, with their nesting level (1 = top-level chapter/part, 2 = its subsections, ...) and the unit number they start in.\nOnly list real headings (titles of parts, chapters, sections, clauses, items), not running page headers, table rows or sentences. If no section starts here, return an empty list.\n\n${body}\n\nReply with JSON only: {\"sections\": [{\"title\": \"...\", \"level\": 1, \"unit\": 0}]}`;\n const reply = parseJsonReply(\n await llm.call({ kind: 'structure', units: shown }, STRUCTURE_SYSTEM, prompt, 2_000),\n );\n const sections = reply?.sections;\n if (!Array.isArray(sections)) {\n throw new Error('structuring reply had no `sections` list');\n }\n const first = window[0]?.index ?? 0;\n const last = window[window.length - 1]?.index ?? first;\n for (const entry of sections) {\n if (entry === null || typeof entry !== 'object') {\n continue;\n }\n const { title, level, unit } = entry as Record<string, unknown>;\n const at = typeof unit === 'number' ? unit : Number(unit);\n if (\n typeof title !== 'string' ||\n !title.trim() ||\n !Number.isInteger(at) ||\n at < first ||\n at > last\n ) {\n continue;\n }\n const previous = headings[headings.length - 1];\n if (previous !== undefined && at < previous.unit) {\n continue;\n }\n headings.push({\n title: collapse(title, 160),\n level: Math.max(1, Math.min(6, Math.floor(typeof level === 'number' ? level : 1))),\n unit: at,\n atUnitStart: false,\n });\n }\n }\n return headings;\n}\n\n/** Run `work` over `items` with at most `limit` in flight. */\nexport async function mapLimit<T, R>(\n items: readonly T[],\n limit: number,\n work: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {\n while (next < items.length) {\n const index = next++;\n results[index] = await work(items[index] as T, index);\n }\n });\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Build a document's navigation tree: a hierarchical table of contents whose nodes carry a title,\n * the page (or section) range they span, and a summary.\n *\n * Structure, cheapest first, never looping:\n *\n * 1. the `outline` the input declares (PDF bookmarks);\n * 2. section titles/levels (`sections` input), or headings detected in the text;\n * 3. the LLM, window by window (only when a document has no usable outline, and only if every\n * window fits `maxStructureCalls` and the budget up front);\n * 4. page groups — always works; the fallback for any failure above.\n *\n * Then summaries: top levels first (they matter most for navigation), each node's text sent once,\n * within the budget; what the budget leaves unsummarized gets the start of its own text. With a\n * `previous` tree, an unchanged input is returned as is (zero calls), and nodes whose title and text\n * are unchanged keep their summary.\n *\n * The result is deterministic for a deterministic model: node ids are depth-first positions, and\n * nothing depends on timing except which summaries a timed-out build got to.\n */\nexport async function buildDocumentTree(\n input: DocumentTreeInput,\n options: BuildDocumentTreeOptions,\n): Promise<BuiltDocumentTree> {\n const started = Date.now();\n const { units, declared, kind } = toUnits(input);\n const maxDepth = Math.max(1, options.maxDepth ?? 4);\n const groupSize = Math.max(1, options.groupSize ?? 5);\n const maxLeafUnits = Math.max(groupSize, options.maxLeafUnits ?? 10);\n const maxChildren = Math.max(2, options.maxChildren ?? 12);\n const minHeadings = Math.max(1, options.minHeadings ?? 3);\n const llmEnabled = options.llm !== undefined;\n const summaries = options.summaries ?? llmEnabled;\n const describe = options.describe ?? llmEnabled;\n const llmStructure = options.llmStructure ?? llmEnabled;\n const detect = options.detectHeadings ?? true;\n const summaryMinChars = options.summaryMinChars ?? 600;\n const summaryInputChars = options.summaryInputChars ?? 8_000;\n\n const unitHashes = units.map((unit) => stableHash(unit.text));\n const fingerprint = stableHash(\n JSON.stringify({\n v: 1,\n kind,\n title: input.title ?? null,\n outline: input.outline ?? null,\n sections:\n input.sections?.map((section) => [\n section.title ?? null,\n section.level ?? null,\n section.page ?? null,\n ]) ?? null,\n units: unitHashes,\n maxDepth,\n groupSize,\n maxLeafUnits,\n maxChildren,\n minHeadings,\n detect,\n summaries,\n describe,\n llmStructure: llmStructure && llmEnabled,\n summaryMinChars,\n summaryInputChars,\n }),\n );\n if (options.previous?.fingerprint === fingerprint) {\n // Same text, same shape: only the access metadata / citation may have moved.\n const { metadata: _metadata, source: _source, ...previous } = options.previous;\n const metadata = options.metadata ?? options.previous.metadata;\n const source = options.source ?? options.previous.source;\n return {\n tree: {\n ...previous,\n ...(metadata !== undefined ? { metadata } : {}),\n ...(source !== undefined ? { source } : {}),\n },\n units,\n unchanged: true,\n };\n }\n\n const budget = new BudgetedLlm(\n options.llm ?? (() => Promise.reject(new Error('no llm'))),\n { ...DEFAULT_BUDGET, ...options.budget },\n options.signal,\n );\n let fallbackReason: string | undefined;\n\n // 1–4: structure.\n let structure: TreeStructureSource = 'flat';\n let headings: DetectedHeading[] = [];\n const running = runningLines(units.map((unit) => unit.text));\n const outline = outlineHeadings(input, units);\n if (outline.length >= Math.min(minHeadings, 2)) {\n structure = 'outline';\n headings = outline;\n } else if (declared !== undefined && declared.length >= 1) {\n structure = 'headings';\n headings = declared;\n } else if (detect !== false && units.length > 0) {\n const found = detectHeadings(\n units.map((unit) => unit.text),\n detect === 'markdown',\n );\n if (found.length >= minHeadings) {\n structure = 'headings';\n headings = found;\n }\n }\n if (structure === 'flat' && llmStructure && llmEnabled && units.length > 1) {\n try {\n const found = await llmHeadings(budget, units, {\n windowChars: options.structureWindowChars ?? 24_000,\n maxCalls: Math.max(1, options.maxStructureCalls ?? 12),\n });\n if (found.length >= Math.min(minHeadings, 2)) {\n structure = 'llm';\n headings = found;\n } else {\n fallbackReason = 'LLM found no usable structure';\n }\n } catch (error) {\n fallbackReason =\n error instanceof TreeBudgetExceededError\n ? `LLM structuring stopped: ${error.message}`\n : `LLM structuring failed: ${(error as Error).message}`;\n }\n }\n\n const draft =\n structure === 'flat'\n ? units.length === 0\n ? []\n : groupNodes(units, 0, units.length - 1, groupSize, maxChildren, maxDepth, running)\n : nestHeadings(headings, units.length);\n const shaped = shape(draft, units, 1, {\n maxDepth,\n maxLeafUnits,\n groupSize,\n maxChildren,\n running,\n });\n\n // Ids, pages and content hashes.\n let counter = 0;\n const finalize = (nodes: DraftNode[]): DocumentTreeNode[] =>\n nodes.map((node) => {\n counter += 1;\n const id = String(counter).padStart(4, '0');\n const pageStart = units[node.unitStart]?.page;\n const pageEnd = units[node.unitEnd]?.page;\n return {\n id,\n title: node.title,\n unitStart: node.unitStart,\n unitEnd: node.unitEnd,\n ...(pageStart !== undefined ? { pageStart } : {}),\n ...(pageEnd !== undefined ? { pageEnd } : {}),\n ...(node.keyItems !== undefined ? { keyItems: node.keyItems } : {}),\n contentHash: stableHash(\n `${node.title}\\u0000${unitHashes.slice(node.unitStart, node.unitEnd + 1).join(',')}`,\n ),\n children: finalize(node.children),\n };\n });\n const nodes = finalize(shaped);\n\n // Summaries: reuse, then top-down (breadth-first) within the budget, then the text itself.\n const previousSummaries = new Map<string, string>();\n if (options.previous !== undefined) {\n for (const { node } of walkTree(options.previous.nodes)) {\n if (\n node.summary !== undefined &&\n (node.summarySource === 'llm' || node.summarySource === 'reused')\n ) {\n previousSummaries.set(node.contentHash, node.summary);\n }\n }\n }\n const breadthFirst: DocumentTreeNode[] = [];\n for (let queue = [...nodes]; queue.length > 0; ) {\n const node = queue.shift() as DocumentTreeNode;\n breadthFirst.push(node);\n queue = queue.concat(node.children);\n }\n let reusedSummaries = 0;\n const pending: DocumentTreeNode[] = [];\n for (const node of breadthFirst) {\n const reused = previousSummaries.get(node.contentHash);\n const own = ownTexts(units, node).join('\\n\\n');\n if (reused !== undefined) {\n node.summary = reused;\n node.summarySource = 'reused';\n reusedSummaries += 1;\n } else if (own.trim().length < summaryMinChars && node.children.length === 0) {\n const text = collapse(own, summaryMinChars);\n if (text) {\n node.summary = text;\n node.summarySource = 'text';\n }\n } else if (summaries && llmEnabled) {\n pending.push(node);\n } else {\n const text = collapse(own, 300);\n if (text) {\n node.summary = text;\n node.summarySource = 'text';\n }\n }\n }\n let summariesCut = false;\n await mapLimit(pending, Math.max(1, options.concurrency ?? 4), async (node) => {\n const own = ownTexts(units, node);\n const childTitles = [...node.children.map((child) => child.title), ...(node.keyItems ?? [])]\n .map((title) => `- ${title}`)\n .join('\\n');\n const excerpt = excerptOf(own, summaryInputChars);\n const prompt = `Summarize this section of ${input.title ? `\"${input.title}\"` : 'a document'} in 2-4 sentences (at most 80 words). Name the specific topics, requirements, clauses, figures and defined terms it covers, so a reader can tell whether it answers their question.\n\nSection title: ${node.title}\n${childTitles ? `Subsections:\\n${childTitles}\\n` : ''}\nSection text:\n${excerpt}\n\nReply with the summary only.`;\n if (!summariesCut && budget.fits(SUMMARY_SYSTEM + prompt, 300)) {\n try {\n const reply = await budget.call(\n { kind: 'summarize', title: node.title, text: excerpt },\n SUMMARY_SYSTEM,\n prompt,\n 300,\n );\n const json = parseJsonReply(reply);\n const summary = typeof json?.summary === 'string' ? json.summary : reply;\n if (summary.trim()) {\n node.summary = collapse(summary, 800);\n node.summarySource = 'llm';\n return;\n }\n } catch (error) {\n if (error instanceof TreeBudgetExceededError) {\n summariesCut = true;\n }\n fallbackReason ??= `summary failed: ${(error as Error).message}`;\n }\n } else {\n summariesCut = true;\n }\n const text = collapse(own.join('\\n\\n'), 300);\n if (text) {\n node.summary = text;\n node.summarySource = 'text';\n }\n });\n if (summariesCut) {\n fallbackReason ??= 'LLM budget exhausted before every node was summarized';\n }\n\n let description: string | undefined;\n const outlineForDescription = nodes.slice(0, 40).map((node) => ({\n title: node.title,\n ...(node.summary !== undefined ? { summary: node.summary } : {}),\n }));\n if (describe && llmEnabled && nodes.length > 0) {\n const prompt = `Write a one-paragraph description (at most 80 words) of the document ${input.title ? `\"${input.title}\"` : ''} from its table of contents below, so a reader can tell which questions it can answer.\n\n${outlineForDescription.map((entry) => `- ${entry.title}${entry.summary ? `: ${entry.summary}` : ''}`).join('\\n')}\n\nReply with the description only.`;\n if (budget.fits(SUMMARY_SYSTEM + prompt, 300)) {\n try {\n description = collapse(\n await budget.call(\n {\n kind: 'describe',\n ...(input.title !== undefined ? { title: input.title } : {}),\n outline: outlineForDescription,\n },\n SUMMARY_SYSTEM,\n prompt,\n 300,\n ),\n 1_000,\n );\n } catch (error) {\n fallbackReason ??= `description failed: ${(error as Error).message}`;\n }\n }\n }\n if (\n description === undefined &&\n options.previous?.description !== undefined &&\n reusedSummaries > 0\n ) {\n description = options.previous.description;\n }\n\n const tree: DocumentTree = {\n documentId: options.documentId,\n version: 1,\n ...(input.title !== undefined ? { title: input.title } : {}),\n ...(description ? { description } : {}),\n structure,\n fingerprint,\n unitCount: units.length,\n nodes,\n stats: {\n llmCalls: budget.calls,\n inputTokens: budget.inputTokens,\n outputTokens: budget.outputTokens,\n durationMs: Date.now() - started,\n ...(fallbackReason !== undefined ? { fallbackReason } : {}),\n budgetExhausted: budget.exhausted,\n reusedSummaries,\n },\n ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),\n ...(options.source !== undefined ? { source: options.source } : {}),\n builtAt: new Date().toISOString(),\n };\n return { tree, units, unchanged: false };\n}\n","import type { TreeLlm, TreeLlmRequest, TreeLlmResponse } from './llm.js';\n\nconst STOP = new Set(\n 'the and for are was were with that this from which what when where who whom how does did has have had not but its into under over than then there their about any all can may must shall should would could will our your they them his her per each other such only also more most'.split(\n ' ',\n ),\n);\n\nfunction terms(text: string): Set<string> {\n return new Set(\n (\n text.toLowerCase().match(/[\\p{L}\\p{N}][\\p{L}\\p{N}.\\-]*[\\p{L}\\p{N}]|[\\p{L}\\p{N}]{3,}/gu) ?? []\n ).filter((term) => term.length >= 3 && !STOP.has(term)),\n );\n}\n\nfunction overlap(query: Set<string>, text: string): number {\n let score = 0;\n for (const term of terms(text)) {\n if (query.has(term)) {\n score += 1;\n }\n }\n return score;\n}\n\n/**\n * A deterministic stand-in for the tree LLM — no model, no network — for tests and offline\n * development. It answers from the structured {@link TreeLlmRequest.task}, never the prose:\n *\n * - `navigate` / `select`: ranks candidates by how many of the question's terms their title and\n * summary share, keeps those sharing any (deepest first on ties in single-pass mode); in a beam,\n * candidates with subsections are expanded, the rest read;\n * - `summarize`: the section's first sentence; `describe`: the top-level titles;\n * - `structure`: lines in ALL CAPS (or markdown headers) become level-1 sections.\n *\n * Wrap it to count calls, inject failures or slow replies when testing budgets.\n */\nexport function keywordTreeLlm(): TreeLlm {\n return async (request: TreeLlmRequest): Promise<TreeLlmResponse> => {\n const { task } = request;\n let text: string;\n switch (task.kind) {\n case 'navigate': {\n const query = terms(task.query);\n const ranked = task.candidates\n .map((candidate, order) => ({\n candidate,\n order,\n score: overlap(query, `${candidate.title} ${candidate.summary ?? ''}`),\n }))\n .filter((entry) => entry.score > 0)\n .sort(\n (a, b) =>\n b.score - a.score ||\n (b.candidate.depth ?? 0) - (a.candidate.depth ?? 0) ||\n a.order - b.order,\n )\n .slice(0, task.maxNodes)\n .map((entry) => entry.candidate);\n text =\n task.mode === 'single-pass'\n ? JSON.stringify({ thinking: 'keyword overlap', read: ranked.map((c) => c.id) })\n : JSON.stringify({\n thinking: 'keyword overlap',\n expand: ranked.filter((c) => c.hasChildren).map((c) => c.id),\n read: ranked.filter((c) => !c.hasChildren).map((c) => c.id),\n });\n break;\n }\n case 'select': {\n const query = terms(task.query);\n const ranked = task.documents\n .map((document) => ({\n id: document.id,\n score: overlap(query, `${document.title ?? ''} ${document.description ?? ''}`),\n }))\n .filter((entry) => entry.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, task.maxDocuments)\n .map((entry) => entry.id);\n text = JSON.stringify({ thinking: 'keyword overlap', documents: ranked });\n break;\n }\n case 'summarize': {\n const flat = task.text.replace(/\\s+/g, ' ').trim();\n const sentence = /^.*?[.!?](\\s|$)/.exec(flat)?.[0] ?? flat;\n text = `${task.title}: ${sentence.slice(0, 200).trim()}`;\n break;\n }\n case 'describe':\n text = `${task.title ?? 'Document'} covering ${task.outline.map((entry) => entry.title).join(', ')}.`;\n break;\n case 'structure': {\n const sections: { title: string; level: number; unit: number }[] = [];\n for (const unit of task.units) {\n for (const line of unit.text.split('\\n')) {\n const trimmed = line.trim();\n const markdown = /^(#{1,6})\\s+(.+)$/.exec(trimmed);\n if (markdown?.[1] !== undefined && markdown[2] !== undefined) {\n sections.push({ title: markdown[2], level: markdown[1].length, unit: unit.unit });\n } else if (/^[A-Z][A-Z0-9 ,.'&-]{3,80}$/.test(trimmed) && /[A-Z]{3}/.test(trimmed)) {\n sections.push({ title: trimmed, level: 1, unit: unit.unit });\n }\n }\n }\n text = JSON.stringify({ sections });\n break;\n }\n }\n return {\n text,\n usage: {\n inputTokens: Math.ceil(request.prompt.length / 4),\n outputTokens: Math.ceil(text.length / 4),\n },\n };\n };\n}\n","import { filterMatchesNothing, matchesFilter } from '../filter.js';\nimport type { DocumentTree, TreeUnit } from './types.js';\n\n/** A stored tree without its nodes — what listing and document selection need. */\nexport interface DocumentTreeHeader {\n documentId: string;\n title?: string;\n description?: string;\n structure: DocumentTree['structure'];\n fingerprint: string;\n unitCount: number;\n metadata?: Record<string, unknown>;\n source?: string;\n builtAt: string;\n}\n\n/**\n * Where navigation trees live: one tree per document plus the units (pages/sections) its node\n * ranges point at, so navigation can read a node's text without going back to the source file.\n *\n * `filter` has **exactly** the vector stores' semantics (see `VectorStore`): every key must match\n * the tree's `metadata`, an array value is match-any, and an empty array denies. It is an access\n * boundary — a navigator given a tenant filter must not read another tenant's tree — so every read\n * that takes one applies it. `readUnits` takes none: call it only for a tree a filtered `get` just\n * returned (which is what the navigator does).\n */\nexport interface DocumentTreeStore {\n /** Store (replace) a document's tree and units. */\n put(tree: DocumentTree, units: TreeUnit[]): Promise<void>;\n /** A document's tree, when it exists **and** passes `filter`. */\n get(documentId: string, filter?: Record<string, unknown>): Promise<DocumentTree | undefined>;\n /** The trees of those of `documentIds` that exist and pass `filter` (any order). */\n getMany(\n documentIds: readonly string[],\n filter?: Record<string, unknown>,\n ): Promise<DocumentTree[]>;\n /** Headers (no nodes) of every tree passing `filter`, or of the listed `documentIds` only. */\n list(options?: {\n filter?: Record<string, unknown>;\n documentIds?: readonly string[];\n limit?: number;\n }): Promise<DocumentTreeHeader[]>;\n /** Units `start..end` (inclusive) of a document, in order. */\n readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]>;\n /** Drop a document's tree and units. Unknown ids are a no-op. */\n remove(documentId: string): Promise<void>;\n}\n\nexport function headerOf(tree: DocumentTree): DocumentTreeHeader {\n return {\n documentId: tree.documentId,\n ...(tree.title !== undefined ? { title: tree.title } : {}),\n ...(tree.description !== undefined ? { description: tree.description } : {}),\n structure: tree.structure,\n fingerprint: tree.fingerprint,\n unitCount: tree.unitCount,\n ...(tree.metadata !== undefined ? { metadata: tree.metadata } : {}),\n ...(tree.source !== undefined ? { source: tree.source } : {}),\n builtAt: tree.builtAt,\n };\n}\n\nfunction passes(tree: DocumentTree, filter: Record<string, unknown> | undefined): boolean {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return true;\n }\n if (filterMatchesNothing(filter)) {\n return false;\n }\n return matchesFilter(tree.metadata, filter);\n}\n\n/** An in-process {@link DocumentTreeStore} — tests, and single-process apps with small corpora. */\nexport class MemoryDocumentTreeStore implements DocumentTreeStore {\n private readonly trees = new Map<string, { tree: DocumentTree; units: TreeUnit[] }>();\n\n async put(tree: DocumentTree, units: TreeUnit[]): Promise<void> {\n this.trees.set(tree.documentId, {\n tree: structuredClone(tree),\n units: units.map((unit) => ({ ...unit })),\n });\n }\n\n async get(\n documentId: string,\n filter?: Record<string, unknown>,\n ): Promise<DocumentTree | undefined> {\n const entry = this.trees.get(documentId);\n return entry !== undefined && passes(entry.tree, filter)\n ? structuredClone(entry.tree)\n : undefined;\n }\n\n async getMany(\n documentIds: readonly string[],\n filter?: Record<string, unknown>,\n ): Promise<DocumentTree[]> {\n const trees: DocumentTree[] = [];\n for (const id of new Set(documentIds)) {\n const tree = await this.get(id, filter);\n if (tree !== undefined) {\n trees.push(tree);\n }\n }\n return trees;\n }\n\n async list(\n options: {\n filter?: Record<string, unknown>;\n documentIds?: readonly string[];\n limit?: number;\n } = {},\n ): Promise<DocumentTreeHeader[]> {\n const ids = options.documentIds !== undefined ? new Set(options.documentIds) : undefined;\n const headers = [...this.trees.values()]\n .filter(\n ({ tree }) =>\n (ids === undefined || ids.has(tree.documentId)) && passes(tree, options.filter),\n )\n .map(({ tree }) => headerOf(tree))\n .sort((a, b) => a.documentId.localeCompare(b.documentId));\n return options.limit !== undefined ? headers.slice(0, options.limit) : headers;\n }\n\n async readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]> {\n const units = this.trees.get(documentId)?.units ?? [];\n return units.slice(Math.max(0, start), Math.max(0, end + 1)).map((unit) => ({ ...unit }));\n }\n\n async remove(documentId: string): Promise<void> {\n this.trees.delete(documentId);\n }\n}\n","import { type PgClient, type PgClientSource, toPgClient } from '../pg-client.js';\nimport { buildWhere, stripNulBytes } from '../pg-vector-store.js';\nimport { type DocumentTreeHeader, type DocumentTreeStore, headerOf } from './store.js';\nimport type { DocumentTree, TreeUnit } from './types.js';\n\nexport interface PgDocumentTreeStoreOptions {\n /** Trees table. Default `agent_rag_trees`. */\n table?: string;\n /** Units (page/section text) table. Default `${table}_units`, i.e. `agent_rag_trees_units`. */\n unitsTable?: string;\n /** Unit rows per multi-row `INSERT`. Default 200. */\n insertBatchSize?: number;\n}\n\ninterface TreeRow {\n document_id: string;\n title: string | null;\n description: string | null;\n structure: DocumentTree['structure'];\n fingerprint: string;\n unit_count: number;\n source: string | null;\n metadata: Record<string, unknown> | string | null;\n tree?: DocumentTree | string;\n built_at: Date | string;\n}\n\nconst IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/;\n\nfunction json<T>(value: T | string | null | undefined): T | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n return typeof value === 'string' ? (JSON.parse(value) as T) : value;\n}\n\n/**\n * A Postgres {@link DocumentTreeStore}: one row per tree (the header columns, `metadata` as `jsonb`\n * for filtering, the nodes as `jsonb`) and one row per unit of text. Filters compile to the same SQL\n * as `PgVectorStore`'s — scalar `@>`, array match-any, empty array denies — so the filter you pass\n * the vector store works here unchanged.\n *\n * Takes the same clients as `PgVectorStore`: a `pg` `Pool`, a Drizzle database, a `postgres.js`\n * `sql`, or a {@link PgClient}. Call {@link PgDocumentTreeStore.ensureSchema} at boot, or copy\n * {@link PgDocumentTreeStore.schemaStatements} into a migration. Needs no extension.\n *\n * Subclassing: every filtered statement goes through {@link PgDocumentTreeStore.whereConditions}, like\n * `PgVectorStore.whereConditions`, so a subclass can map a filter onto its own columns.\n */\nexport class PgDocumentTreeStore implements DocumentTreeStore {\n protected readonly client: PgClient;\n protected readonly table: string;\n protected readonly unitsTable: string;\n private readonly insertBatchSize: number;\n\n constructor(client: PgClientSource, options: PgDocumentTreeStoreOptions = {}) {\n this.client = toPgClient(client);\n this.table = options.table ?? 'agent_rag_trees';\n this.unitsTable = options.unitsTable ?? `${this.table}_units`;\n for (const name of [this.table, this.unitsTable]) {\n if (!IDENTIFIER.test(name)) {\n throw new Error(`PgDocumentTreeStore: invalid table name ${JSON.stringify(name)}`);\n }\n }\n this.insertBatchSize = Math.max(1, Math.floor(options.insertBatchSize ?? 200));\n }\n\n /** The idempotent DDL {@link PgDocumentTreeStore.ensureSchema} runs, in order — for a migration. */\n schemaStatements(): string[] {\n const index = this.table.replace('.', '_');\n return [\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n document_id TEXT PRIMARY KEY,\n title TEXT,\n description TEXT,\n structure TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n unit_count INTEGER NOT NULL,\n source TEXT,\n metadata JSONB,\n tree JSONB NOT NULL,\n built_at TIMESTAMPTZ NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS ${index}_metadata_idx ON ${this.table} USING gin (metadata)`,\n `CREATE TABLE IF NOT EXISTS ${this.unitsTable} (\n document_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n page INTEGER,\n text TEXT NOT NULL,\n PRIMARY KEY (document_id, idx)\n )`,\n ];\n }\n\n async ensureSchema(): Promise<void> {\n for (const statement of this.schemaStatements()) {\n await this.client.query(statement);\n }\n }\n\n /**\n * The SQL conditions a metadata `filter` compiles to (ANDed), appending bindings to `params`.\n * Override to map a filter onto your own columns. Return `['false']` to deny.\n */\n protected whereConditions(\n filter: Record<string, unknown> | undefined,\n params: unknown[],\n ): string[] {\n return buildWhere(filter, params);\n }\n\n /** Replace the tree and its units in one transaction (when the client has one). */\n async put(tree: DocumentTree, units: TreeUnit[]): Promise<void> {\n const clean = stripNulBytes(tree);\n const { metadata, ...rest } = clean;\n const write = async (client: PgClient) => {\n await client.query(`DELETE FROM ${this.unitsTable} WHERE document_id = $1`, [\n clean.documentId,\n ]);\n for (let start = 0; start < units.length; start += this.insertBatchSize) {\n const batch = units.slice(start, start + this.insertBatchSize);\n const params: unknown[] = [];\n const rows = batch.map((unit) => {\n params.push(clean.documentId, unit.index, unit.page ?? null, stripNulBytes(unit.text));\n const at = params.length;\n return `($${at - 3}, $${at - 2}, $${at - 1}, $${at})`;\n });\n await client.query(\n `INSERT INTO ${this.unitsTable} (document_id, idx, page, text) VALUES ${rows.join(', ')}`,\n params,\n );\n }\n await client.query(\n `INSERT INTO ${this.table}\n (document_id, title, description, structure, fingerprint, unit_count, source, metadata, tree, built_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10)\n ON CONFLICT (document_id) DO UPDATE SET\n title = EXCLUDED.title, description = EXCLUDED.description, structure = EXCLUDED.structure,\n fingerprint = EXCLUDED.fingerprint, unit_count = EXCLUDED.unit_count, source = EXCLUDED.source,\n metadata = EXCLUDED.metadata, tree = EXCLUDED.tree, built_at = EXCLUDED.built_at`,\n [\n clean.documentId,\n clean.title ?? null,\n clean.description ?? null,\n clean.structure,\n clean.fingerprint,\n clean.unitCount,\n clean.source ?? null,\n metadata === undefined ? null : JSON.stringify(metadata),\n JSON.stringify(rest),\n clean.builtAt,\n ],\n );\n };\n const transaction = this.client.transaction?.bind(this.client);\n await (transaction !== undefined ? transaction(write) : write(this.client));\n }\n\n async get(\n documentId: string,\n filter?: Record<string, unknown>,\n ): Promise<DocumentTree | undefined> {\n return (await this.getMany([documentId], filter))[0];\n }\n\n async getMany(\n documentIds: readonly string[],\n filter?: Record<string, unknown>,\n ): Promise<DocumentTree[]> {\n if (documentIds.length === 0) {\n return [];\n }\n const params: unknown[] = [[...new Set(documentIds)]];\n const conditions = ['document_id = ANY($1::text[])', ...this.whereConditions(filter, params)];\n const rows = await this.client.query<TreeRow>(\n `SELECT document_id, metadata, tree FROM ${this.table} WHERE ${conditions.join(' AND ')}`,\n params,\n );\n return rows.map((row) => {\n const tree = json<Omit<DocumentTree, 'metadata'>>(row.tree) as DocumentTree;\n const metadata = json<Record<string, unknown>>(row.metadata);\n return { ...tree, ...(metadata !== undefined ? { metadata } : {}) };\n });\n }\n\n async list(\n options: {\n filter?: Record<string, unknown>;\n documentIds?: readonly string[];\n limit?: number;\n } = {},\n ): Promise<DocumentTreeHeader[]> {\n const params: unknown[] = [];\n const conditions = this.whereConditions(options.filter, params);\n if (options.documentIds !== undefined) {\n params.push([...options.documentIds]);\n conditions.push(`document_id = ANY($${params.length}::text[])`);\n }\n let limit = '';\n if (options.limit !== undefined) {\n params.push(Math.max(0, Math.floor(options.limit)));\n limit = `LIMIT $${params.length}`;\n }\n const rows = await this.client.query<TreeRow>(\n `SELECT document_id, title, description, structure, fingerprint, unit_count, source, metadata, built_at\n FROM ${this.table}\n ${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}\n ORDER BY document_id ${limit}`,\n params,\n );\n return rows.map((row) => {\n const metadata = json<Record<string, unknown>>(row.metadata);\n return headerOf({\n documentId: row.document_id,\n version: 1,\n ...(row.title !== null ? { title: row.title } : {}),\n ...(row.description !== null ? { description: row.description } : {}),\n structure: row.structure,\n fingerprint: row.fingerprint,\n unitCount: Number(row.unit_count),\n nodes: [],\n stats: {\n llmCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n durationMs: 0,\n budgetExhausted: false,\n reusedSummaries: 0,\n },\n ...(metadata !== undefined ? { metadata } : {}),\n ...(row.source !== null ? { source: row.source } : {}),\n builtAt: row.built_at instanceof Date ? row.built_at.toISOString() : String(row.built_at),\n });\n });\n }\n\n async readUnits(documentId: string, start: number, end: number): Promise<TreeUnit[]> {\n const rows = await this.client.query<{ idx: number; page: number | null; text: string }>(\n `SELECT idx, page, text FROM ${this.unitsTable}\n WHERE document_id = $1 AND idx BETWEEN $2 AND $3 ORDER BY idx`,\n [documentId, Math.max(0, start), end],\n );\n return rows.map((row) => ({\n index: Number(row.idx),\n text: row.text,\n ...(row.page !== null ? { page: Number(row.page) } : {}),\n }));\n }\n\n async remove(documentId: string): Promise<void> {\n const work = async (client: PgClient) => {\n await client.query(`DELETE FROM ${this.unitsTable} WHERE document_id = $1`, [documentId]);\n await client.query(`DELETE FROM ${this.table} WHERE document_id = $1`, [documentId]);\n };\n const transaction = this.client.transaction?.bind(this.client);\n await (transaction !== undefined ? transaction(work) : work(this.client));\n }\n}\n","import { type BuildDocumentTreeOptions, buildDocumentTree, toUnits } from './build.js';\nimport type { DocumentTreeStore } from './store.js';\nimport type { DocumentTree, DocumentTreeInput } from './types.js';\n\nexport interface IndexDocumentTreeOptions extends Omit<BuildDocumentTreeOptions, 'previous'> {\n store: DocumentTreeStore;\n /**\n * Only documents with at least this many units (pages, or sections) get a tree; a shorter one is\n * skipped and any tree it had is removed. Default 20 — tree navigation pays off on long\n * structured documents and costs latency and LLM spend for nothing on short notes.\n */\n minUnits?: number;\n /** …or at least this many characters, whichever is met first. Default 60 000 (~15k tokens). */\n minChars?: number;\n}\n\nexport type IndexDocumentTreeResult =\n | { status: 'skipped'; reason: string }\n | { status: 'unchanged'; tree: DocumentTree }\n | { status: 'built'; tree: DocumentTree };\n\n/**\n * Build (or rebuild) a document's tree and store it — the call to make at upload/ingestion time,\n * next to chunking and embedding. Short documents are skipped (see `minUnits`/`minChars`); an\n * unchanged document costs one store read and no LLM call; a changed one reuses the summaries of\n * its unchanged sections. Never throws for a document the LLM cannot structure: the tree falls back\n * to page groups and `tree.stats.fallbackReason` says why. Store errors do propagate.\n */\nexport async function indexDocumentTree(\n input: DocumentTreeInput,\n options: IndexDocumentTreeOptions,\n): Promise<IndexDocumentTreeResult> {\n const { store, minUnits = 20, minChars = 60_000, ...build } = options;\n const { units } = toUnits(input);\n const chars = units.reduce((total, unit) => total + unit.text.length, 0);\n if (units.length < minUnits && chars < minChars) {\n await store.remove(options.documentId);\n return {\n status: 'skipped',\n reason: `${units.length} units / ${chars} chars is below the threshold (${minUnits} units or ${minChars} chars)`,\n };\n }\n const previous = await store.get(options.documentId);\n const built = await buildDocumentTree(input, {\n ...build,\n ...(previous !== undefined ? { previous } : {}),\n });\n if (built.unchanged) {\n const metadataMoved =\n JSON.stringify(previous?.metadata ?? null) !== JSON.stringify(built.tree.metadata ?? null) ||\n previous?.source !== built.tree.source;\n if (metadataMoved) {\n await store.put(built.tree, built.units);\n }\n return { status: 'unchanged', tree: built.tree };\n }\n await store.put(built.tree, built.units);\n return { status: 'built', tree: built.tree };\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport type { RetrievalDescriptor } from '../retrieval-descriptor.js';\nimport { mapLimit } from './build.js';\nimport {\n BudgetedLlm,\n type TreeBudget,\n TreeBudgetExceededError,\n type TreeLlm,\n type TreeLlmCandidate,\n parseJsonReply,\n stringList,\n} from './llm.js';\nimport type { DocumentTreeStore } from './store.js';\nimport type { DocumentTree, DocumentTreeNode } from './types.js';\nimport { indexTree, walkTree } from './types.js';\n\nexport interface TreeNavigationRetrieverOptions {\n store: DocumentTreeStore;\n llm: TreeLlm;\n /**\n * A tree whose outline (titles, pages, summaries) fits in this many characters is navigated in\n * **one** call that sees the whole outline — PageIndex's tree search, the variant measured in the\n * benchmark. Bigger trees are walked level by level (a beam). Default 40 000 (~10k tokens).\n */\n singlePassMaxChars?: number;\n /** Nodes the navigator may expand or read per beam step. Default 4. */\n beamWidth?: number;\n /** Beam levels, at most. Nodes still marked \"expand\" at the last level are read whole. Default 4. */\n maxSteps?: number;\n /** Nodes returned per document, at most. Default 5. */\n maxNodes?: number;\n /**\n * Hard per-document limits for one navigation. Defaults: 6 calls, 60k input tokens, 4k output\n * tokens, 60 s. Running out mid-walk returns what was chosen so far (the nodes last marked for\n * expansion are read whole) and records `stoppedBy: 'budget'`.\n */\n budget?: TreeBudget;\n /** Characters of text per returned node. Default 12 000. */\n maxPassageChars?: number;\n /** Units (pages/sections) read per returned node, at most. Default 25. */\n maxUnitsPerNode?: number;\n /** Characters across all passages of one navigation (every document). Default 48 000 (~12k tokens). */\n maxContextChars?: number;\n /**\n * {@link TreeNavigationRetriever.retrieve} with no first stage: documents navigated per query. When\n * more trees pass the filter, the LLM picks this many from their titles and descriptions\n * (PageIndex's document selection). Default 3.\n */\n maxDocuments?: number;\n /** Trees shown to document selection, at most. Default 200. */\n maxSelectionCandidates?: number;\n /** Documents navigated in parallel. Default 3. */\n concurrency?: number;\n}\n\n/** One step of a navigation, for the audit trail. */\nexport interface NavigationStep {\n step: number;\n /** Node ids shown to the LLM this step. */\n shown: string[];\n /** Node ids it chose to open further. */\n expanded: string[];\n /** Node ids it chose to read. */\n read: string[];\n /** The LLM's stated reasoning, verbatim (capped). */\n reasoning?: string;\n}\n\n/** A node navigation chose, with the path that led to it. */\nexport interface NavigatedNode {\n nodeId: string;\n title: string;\n /** Ancestors from the top level down, then the node itself. */\n path: { id: string; title: string }[];\n pageStart?: number;\n pageEnd?: number;\n unitStart: number;\n unitEnd: number;\n}\n\n/** The auditable record of navigating one document. */\nexport interface DocumentNavigation {\n documentId: string;\n title?: string;\n mode: 'single-pass' | 'beam';\n steps: NavigationStep[];\n nodes: NavigatedNode[];\n llmCalls: number;\n inputTokens: number;\n outputTokens: number;\n /** Why the walk stopped early, when it did. */\n stoppedBy?: 'budget' | 'timeout' | 'maxSteps' | 'error' | 'not-found';\n error?: string;\n}\n\nexport interface NavigationResult {\n passages: Passage[];\n documents: DocumentNavigation[];\n /** Set when {@link TreeNavigationRetriever.retrieve} had the LLM pick the documents. */\n selection?: { candidates: number; selected: string[]; reasoning?: string; error?: string };\n}\n\nexport interface NavigateOptions {\n /** Access filter applied to every tree read (vector-store semantics). */\n filter?: Record<string, unknown>;\n /** Overrides the retriever's per-document budget. */\n budget?: TreeBudget;\n signal?: AbortSignal;\n}\n\nconst NAVIGATE_SYSTEM =\n 'You navigate the table of contents of a document to find where a question is answered. You answer with JSON only.';\n\nconst DEFAULT_BUDGET = {\n maxCalls: 6,\n maxInputTokens: 60_000,\n maxOutputTokens: 4_000,\n timeoutMs: 60_000,\n} as const;\n\nfunction pages(node: Pick<DocumentTreeNode, 'pageStart' | 'pageEnd'>): string | undefined {\n if (node.pageStart === undefined) {\n return undefined;\n }\n return node.pageEnd === undefined || node.pageEnd === node.pageStart\n ? `p. ${node.pageStart}`\n : `pp. ${node.pageStart}-${node.pageEnd}`;\n}\n\nfunction candidate(node: DocumentTreeNode, summaryChars: number, depth?: number): TreeLlmCandidate {\n const shownPages = pages(node);\n let summary = node.summary;\n if (summary !== undefined && summary.length > summaryChars) {\n summary = `${summary.slice(0, Math.max(0, summaryChars - 1))}…`;\n }\n if (node.keyItems !== undefined && node.keyItems.length > 0 && summaryChars > 0) {\n summary = `${summary ?? ''}${summary ? ' ' : ''}Includes: ${node.keyItems.slice(0, 12).join('; ')}`;\n }\n return {\n id: node.id,\n title: node.title,\n ...(summary && summaryChars > 0 ? { summary } : {}),\n ...(shownPages !== undefined ? { pages: shownPages } : {}),\n ...(depth !== undefined ? { depth } : { hasChildren: node.children.length > 0 }),\n };\n}\n\nfunction render(candidates: TreeLlmCandidate[]): string {\n return candidates\n .map((entry) => {\n const indent = ' '.repeat(entry.depth ?? 0);\n const meta = [entry.pages, entry.hasChildren ? 'has subsections' : undefined]\n .filter(Boolean)\n .join(', ');\n return `${indent}[${entry.id}] ${entry.title}${meta ? ` (${meta})` : ''}${entry.summary ? ` — ${entry.summary}` : ''}`;\n })\n .join('\\n');\n}\n\n/** Candidates rendered within `maxChars`: full summaries, then shorter, then titles only. */\nfunction fit(\n nodes: { node: DocumentTreeNode; depth?: number }[],\n maxChars: number,\n): { candidates: TreeLlmCandidate[]; text: string } {\n let last: { candidates: TreeLlmCandidate[]; text: string } | undefined;\n for (const summaryChars of [400, 160, 0]) {\n const candidates = nodes.map(({ node, depth }) => candidate(node, summaryChars, depth));\n const text = render(candidates);\n last = { candidates, text };\n if (text.length <= maxChars) {\n break;\n }\n }\n return last as { candidates: TreeLlmCandidate[]; text: string };\n}\n\n/**\n * Tree navigation retrieval (\"vectorless\", reasoning-based — PageIndex's method): an LLM reads a\n * document's table of contents (titles, page ranges, summaries — no body text) and picks the\n * sections that answer the question; their text comes back as passages, with the path taken and the\n * model's reasoning recorded so the choice is auditable.\n *\n * A small tree is navigated in one call over the whole outline. A large one is walked level by\n * level: the model sees one level of candidates, opens (`expand`) the promising ones and takes\n * (`read`) the ones that already answer, until it reaches leaves, runs out of `maxSteps`, or runs\n * out of its hard per-document {@link TreeBudget} — in which case it returns what it had.\n *\n * It pays off on **long structured documents** (regulations, contracts, filings, manuals) where\n * chunk ranking struggles among hundreds of look-alike passages; on short notes it costs seconds and\n * LLM spend for nothing. Compose it behind a first stage with {@link TwoStageRetriever}, or give it\n * to the agent as a tool with {@link createNavigateDocumentTool}.\n *\n * As a plain `Retriever`, `retrieve(query, { filter })` navigates the trees that pass `filter`:\n * all of them when there are at most `maxDocuments`, otherwise the ones the LLM picks from their\n * descriptions.\n */\nexport class TreeNavigationRetriever implements Retriever {\n readonly store: DocumentTreeStore;\n private readonly llm: TreeLlm;\n\n constructor(private readonly options: TreeNavigationRetrieverOptions) {\n this.store = options.store;\n this.llm = options.llm;\n }\n\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'tree-navigation' };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const result = await this.search(\n query,\n options.filter !== undefined ? { filter: options.filter } : {},\n );\n return result.passages.slice(0, options.topK ?? result.passages.length);\n }\n\n /** {@link retrieve} with the audit trail: which documents, which nodes, why. */\n async search(query: string, options: NavigateOptions = {}): Promise<NavigationResult> {\n const maxDocuments = Math.max(1, this.options.maxDocuments ?? 3);\n const maxCandidates = Math.max(maxDocuments, this.options.maxSelectionCandidates ?? 200);\n const headers = await this.store.list({\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n limit: maxCandidates,\n });\n if (headers.length <= maxDocuments) {\n return this.navigate(\n query,\n headers.map((header) => header.documentId),\n options,\n );\n }\n const budget = new BudgetedLlm(\n this.llm,\n { ...DEFAULT_BUDGET, ...this.options.budget, ...options.budget },\n options.signal,\n );\n const documents = headers.map((header) => ({\n id: header.documentId,\n ...(header.title !== undefined ? { title: header.title } : {}),\n ...(header.description !== undefined\n ? { description: header.description.slice(0, 600) }\n : {}),\n }));\n const prompt = `Select the documents that may contain the answer to the question, most relevant first, at most ${maxDocuments}.\n\nQuestion: ${query}\n\nDocuments:\n${documents.map((document) => `[${document.id}] ${document.title ?? ''}${document.description ? ` — ${document.description}` : ''}`).join('\\n')}\n\nReply with JSON only: {\"thinking\": \"<why>\", \"documents\": [\"<document id>\", ...]} — an empty list if none is relevant.`;\n const known = new Set(documents.map((document) => document.id));\n try {\n const reply = parseJsonReply(\n await budget.call(\n { kind: 'select', query, documents, maxDocuments },\n NAVIGATE_SYSTEM,\n prompt,\n 800,\n ),\n );\n const selected = [\n ...new Set(stringList(reply?.documents ?? reply?.answer).filter((id) => known.has(id))),\n ].slice(0, maxDocuments);\n const result = await this.navigate(query, selected, options);\n const reasoning =\n typeof reply?.thinking === 'string' ? reply.thinking.slice(0, 2_000) : undefined;\n return {\n ...result,\n selection: {\n candidates: documents.length,\n selected,\n ...(reasoning !== undefined ? { reasoning } : {}),\n },\n };\n } catch (error) {\n return {\n passages: [],\n documents: [],\n selection: { candidates: documents.length, selected: [], error: (error as Error).message },\n };\n }\n }\n\n /**\n * Navigate the given documents (in parallel, up to `concurrency`) and return their passages in\n * the order given, within `maxContextChars`. Documents without a tree, or whose tree fails\n * `filter`, yield no passages and `stoppedBy: 'not-found'`.\n */\n async navigate(\n query: string,\n documentIds: readonly string[],\n options: NavigateOptions = {},\n ): Promise<NavigationResult> {\n const ids = [...new Set(documentIds)];\n const trees = new Map(\n (await this.store.getMany(ids, options.filter)).map((tree) => [tree.documentId, tree]),\n );\n const navigations = await mapLimit(\n ids,\n Math.max(1, this.options.concurrency ?? 3),\n async (id) => {\n const tree = trees.get(id);\n if (tree === undefined) {\n return {\n navigation: {\n documentId: id,\n mode: 'single-pass',\n steps: [],\n nodes: [],\n llmCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n stoppedBy: 'not-found',\n } satisfies DocumentNavigation,\n passages: [] as Passage[],\n };\n }\n const navigation = await this.walk(query, tree, options);\n return { navigation, passages: await this.passagesOf(tree, navigation) };\n },\n );\n // Share the context budget across documents in the order given.\n const maxContextChars = this.options.maxContextChars ?? 48_000;\n let used = 0;\n const passages: Passage[] = [];\n for (const entry of navigations) {\n for (const passage of entry.passages) {\n if (used >= maxContextChars) {\n break;\n }\n const text = passage.text.slice(0, maxContextChars - used);\n used += text.length;\n passages.push({ ...passage, text });\n }\n }\n return { passages, documents: navigations.map((entry) => entry.navigation) };\n }\n\n private async walk(\n query: string,\n tree: DocumentTree,\n options: NavigateOptions,\n ): Promise<DocumentNavigation> {\n const budget = new BudgetedLlm(\n this.llm,\n { ...DEFAULT_BUDGET, ...this.options.budget, ...options.budget },\n options.signal,\n );\n const maxNodes = Math.max(1, this.options.maxNodes ?? 5);\n const singlePassMaxChars = this.options.singlePassMaxChars ?? 40_000;\n const index = indexTree(tree);\n const whole = [...walkTree(tree.nodes)].map(({ node, path }) => ({ node, depth: path.length }));\n const outline = fit(whole, Number.POSITIVE_INFINITY);\n const title = tree.title ?? tree.source ?? tree.documentId;\n const steps: NavigationStep[] = [];\n const chosen: string[] = [];\n let stoppedBy: DocumentNavigation['stoppedBy'];\n let error: string | undefined;\n const mode: DocumentNavigation['mode'] =\n outline.text.length <= singlePassMaxChars ? 'single-pass' : 'beam';\n\n const stop = (caught: unknown) => {\n if (caught instanceof TreeBudgetExceededError) {\n stoppedBy = caught.limit === 'timeout' ? 'timeout' : 'budget';\n } else {\n stoppedBy = 'error';\n }\n error = (caught as Error).message;\n };\n\n if (mode === 'single-pass') {\n const prompt = `Find the sections of the document that are likely to contain the answer to the question. Prefer the most specific (deepest) sections; list at most ${maxNodes}, most relevant first.\n\nQuestion: ${query}\n\nDocument: ${title}${tree.description ? `\\nAbout: ${tree.description}` : ''}\nTable of contents ([id] title (pages) — summary):\n${outline.text}\n\nReply with JSON only: {\"thinking\": \"<which sections answer and why>\", \"read\": [\"<id>\", ...]} — an empty list if none does.`;\n try {\n const reply = requireJson(\n await budget.call(\n { kind: 'navigate', query, mode, candidates: outline.candidates, maxNodes },\n NAVIGATE_SYSTEM,\n prompt,\n 1_500,\n ),\n );\n const read = [...new Set(stringList(reply?.read ?? reply?.node_list ?? reply?.nodes))]\n .map((id) => resolveId(index, id))\n .filter((id): id is string => id !== undefined)\n .slice(0, maxNodes);\n chosen.push(...read);\n steps.push({\n step: 1,\n shown: outline.candidates.map((entry) => entry.id),\n expanded: [],\n read,\n ...reasoningOf(reply),\n });\n } catch (caught) {\n stop(caught);\n }\n } else {\n const beamWidth = Math.max(1, this.options.beamWidth ?? 4);\n const maxSteps = Math.max(1, this.options.maxSteps ?? 4);\n let frontier = tree.nodes;\n let pendingExpand: string[] = [];\n for (let step = 1; frontier.length > 0 && chosen.length < maxNodes; step++) {\n if (step > maxSteps) {\n stoppedBy = 'maxSteps';\n break;\n }\n const shown = fit(\n frontier.map((node) => ({ node })),\n singlePassMaxChars,\n );\n const path = index.get(frontier[0]?.id ?? '')?.path ?? [];\n const where =\n path.length > 0 ? `\\nYou are inside: ${path.map((node) => node.title).join(' > ')}` : '';\n const prompt = `Find where the question is answered in the document by walking its table of contents.\nFor each relevant section below, either \"expand\" it (open its subsections, only for sections that have subsections) or \"read\" it (its text answers the question). Choose at most ${beamWidth} in total, most relevant first; choose none if nothing here is relevant.\n\nQuestion: ${query}\n\nDocument: ${title}${tree.description ? `\\nAbout: ${tree.description}` : ''}${where}\nSections ([id] title (pages) — summary):\n${shown.text}\n\nReply with JSON only: {\"thinking\": \"<why>\", \"expand\": [\"<id>\", ...], \"read\": [\"<id>\", ...]}`;\n let reply: Record<string, unknown> | undefined;\n try {\n reply = requireJson(\n await budget.call(\n { kind: 'navigate', query, mode, candidates: shown.candidates, maxNodes: beamWidth },\n NAVIGATE_SYSTEM,\n prompt,\n 1_200,\n ),\n );\n } catch (caught) {\n stop(caught);\n break;\n }\n const onFrontier = new Set(frontier.map((node) => node.id));\n const pick = (value: unknown) =>\n [...new Set(stringList(value))]\n .map((id) => resolveId(index, id))\n .filter((id): id is string => id !== undefined && onFrontier.has(id));\n let expand = pick(reply?.expand);\n let read = pick(reply?.read);\n // A leaf cannot be expanded: reading it is what was meant.\n read = [\n ...read,\n ...expand.filter((id) => (index.get(id)?.node.children.length ?? 0) === 0),\n ];\n expand = expand.filter((id) => (index.get(id)?.node.children.length ?? 0) > 0);\n const limited = [...expand, ...read].slice(0, beamWidth);\n expand = expand.filter((id) => limited.includes(id));\n read = [...new Set(read.filter((id) => limited.includes(id)))];\n steps.push({\n step,\n shown: shown.candidates.map((entry) => entry.id),\n expanded: expand,\n read,\n ...reasoningOf(reply),\n });\n chosen.push(...read);\n pendingExpand = expand;\n frontier = expand.flatMap((id) => index.get(id)?.node.children ?? []);\n if (frontier.length === 0) {\n pendingExpand = [];\n }\n }\n // Anything still marked for expansion when the walk stopped is read whole.\n if (stoppedBy !== undefined) {\n chosen.push(...pendingExpand);\n }\n }\n\n // A section chosen together with one of its own subsections adds nothing but length: the\n // more specific choice wins.\n const unique = [...new Set(chosen)];\n const specific = unique.filter(\n (id) =>\n !unique.some(\n (other) => other !== id && index.get(other)?.path.some((node) => node.id === id),\n ),\n );\n const nodes = specific.slice(0, maxNodes).flatMap((id) => {\n const entry = index.get(id);\n if (entry === undefined) {\n return [];\n }\n const { node, path } = entry;\n return [\n {\n nodeId: node.id,\n title: node.title,\n path: [...path, node].map((step) => ({ id: step.id, title: step.title })),\n ...(node.pageStart !== undefined ? { pageStart: node.pageStart } : {}),\n ...(node.pageEnd !== undefined ? { pageEnd: node.pageEnd } : {}),\n unitStart: node.unitStart,\n unitEnd: node.unitEnd,\n } satisfies NavigatedNode,\n ];\n });\n return {\n documentId: tree.documentId,\n ...(tree.title !== undefined ? { title: tree.title } : {}),\n mode,\n steps,\n nodes,\n llmCalls: budget.calls,\n inputTokens: budget.inputTokens,\n outputTokens: budget.outputTokens,\n ...(stoppedBy !== undefined ? { stoppedBy } : {}),\n ...(error !== undefined ? { error } : {}),\n };\n }\n\n private async passagesOf(tree: DocumentTree, navigation: DocumentNavigation): Promise<Passage[]> {\n const maxPassageChars = this.options.maxPassageChars ?? 12_000;\n const maxUnits = Math.max(1, this.options.maxUnitsPerNode ?? 25);\n const reasoning = navigation.steps\n .map((step) => step.reasoning)\n .filter((text): text is string => text !== undefined)\n .join('\\n');\n const passages: Passage[] = [];\n for (const [rank, node] of navigation.nodes.entries()) {\n const units = await this.store.readUnits(\n tree.documentId,\n node.unitStart,\n Math.min(node.unitEnd, node.unitStart + maxUnits - 1),\n );\n let text = units.map((unit) => unit.text).join('\\n\\n');\n if (text.length > maxPassageChars) {\n text = `${text.slice(0, maxPassageChars - 1)}…`;\n }\n passages.push({\n id: `${tree.documentId}#node:${node.nodeId}`,\n text,\n // Rank within the document's choice; not a similarity.\n score: 1 / (rank + 1),\n source: tree.source ?? tree.title ?? tree.documentId,\n metadata: {\n ...tree.metadata,\n documentId: tree.documentId,\n nodeId: node.nodeId,\n title: node.title,\n ...(node.pageStart !== undefined ? { pageStart: node.pageStart } : {}),\n ...(node.pageEnd !== undefined ? { pageEnd: node.pageEnd } : {}),\n path: node.path,\n retrieval: 'tree-navigation',\n ...(reasoning ? { reasoning: reasoning.slice(0, 2_000) } : {}),\n },\n });\n }\n return passages;\n }\n}\n\n/** Accept `0003`, `3`, `\"[0003]\"` for node `0003`. */\nfunction resolveId(index: Map<string, unknown>, raw: string): string | undefined {\n const id = raw.trim().replace(/^\\[|\\]$/g, '');\n if (index.has(id)) {\n return id;\n }\n if (/^\\d+$/.test(id)) {\n const padded = id.padStart(4, '0');\n if (index.has(padded)) {\n return padded;\n }\n }\n return undefined;\n}\n\n/** The reply's JSON object, or an error naming what came back (a truncated or chatty reply). */\nfunction requireJson(reply: string): Record<string, unknown> {\n const json = parseJsonReply(reply);\n if (json === undefined) {\n throw new Error(`navigation reply was not JSON: ${JSON.stringify(reply.slice(0, 120))}`);\n }\n return json;\n}\n\nfunction reasoningOf(reply: Record<string, unknown> | undefined): { reasoning?: string } {\n const thinking = reply?.thinking ?? reply?.reasoning;\n return typeof thinking === 'string' && thinking.trim()\n ? { reasoning: thinking.slice(0, 2_000) }\n : {};\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport type { RetrievalDescriptor } from '../retrieval-descriptor.js';\nimport { documentIdOf } from '../vector-store.js';\nimport type { TreeBudget } from './llm.js';\nimport type { DocumentNavigation, TreeNavigationRetriever } from './navigate.js';\n\nexport interface TwoStageRetrieverOptions {\n /** Passages pulled from the first stage. Default 20. */\n fetchTopK?: number;\n /**\n * Only the first this-many distinct documents of the first stage are considered for navigation.\n * Default 3 — past the top few, a document is more likely a large irrelevant tree (the costliest\n * failure measured: navigating FAR Part 52 because it ranked fourth) than the answer.\n */\n considerDocuments?: number;\n /** Of those, navigate at most this many (the ones that have a tree). Default 2. */\n navigateDocuments?: number;\n /** Per-document navigation budget (overrides the navigator's). */\n budget?: TreeBudget;\n /** Keep the first stage's chunks of a navigated document after its tree passages. Default `false`. */\n keepFirstStageChunks?: boolean;\n /**\n * The document a first-stage passage belongs to. Default: `metadata.documentId` when it is a\n * string, else the chunk id without its `#<n>` suffix (the id scheme `chunkDocuments` produces).\n */\n documentIdOf?: (passage: Passage) => string;\n /** Called with each navigation's audit trail (log it, trace it, show it). */\n onNavigation?: (navigation: DocumentNavigation) => void;\n}\n\nfunction defaultDocumentIdOf(passage: Passage): string {\n const fromMetadata = passage.metadata?.documentId;\n return typeof fromMetadata === 'string' ? fromMetadata : documentIdOf(passage.id);\n}\n\n/**\n * Hybrid first stage, tree navigation second — the design the benchmark recommends: keep fast\n * chunk search (hybrid/lexical/dense, 0.3 s, no LLM) for picking **documents**, and navigate the\n * table of contents only inside the top **long** documents, where chunk ranking is what fails.\n *\n * 1. The first stage retrieves `fetchTopK` passages.\n * 2. Among its first `considerDocuments` distinct documents, those with a tree in the navigator's\n * store (build trees only for long documents, see `indexDocumentTree`'s `minUnits`) are long;\n * the first `navigateDocuments` of them are navigated, in parallel, each on its own budget.\n * 3. The output keeps the first stage's order: a navigated document's section passages take the\n * place of its chunks (at its best chunk's rank); short documents keep their chunks. A navigation\n * that fails or finds nothing leaves that document's chunks in place — never worse than stage one.\n *\n * `filter` is passed to both stages, so the access boundary holds on either path.\n */\nexport class TwoStageRetriever implements Retriever {\n constructor(\n private readonly firstStage: Retriever,\n private readonly navigator: TreeNavigationRetriever,\n private readonly options: TwoStageRetrieverOptions = {},\n ) {}\n\n describeRetrieval(): RetrievalDescriptor {\n return { retriever: 'two-stage' };\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return (await this.search(query, options)).passages;\n }\n\n /** {@link retrieve} with the navigations that were run. */\n async search(\n query: string,\n options: RetrieveOptions = {},\n ): Promise<{ passages: Passage[]; navigations: DocumentNavigation[] }> {\n const topK = options.topK ?? 5;\n const docOf = this.options.documentIdOf ?? defaultDocumentIdOf;\n const first = await this.firstStage.retrieve(query, {\n topK: Math.max(topK, this.options.fetchTopK ?? 20),\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n const ranked: string[] = [];\n for (const passage of first) {\n const id = docOf(passage);\n if (!ranked.includes(id)) {\n ranked.push(id);\n }\n }\n const considered = ranked.slice(0, Math.max(1, this.options.considerDocuments ?? 3));\n const withTrees = new Set(\n (\n await this.navigator.store.list({\n documentIds: considered,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n })\n ).map((header) => header.documentId),\n );\n const toNavigate = considered\n .filter((id) => withTrees.has(id))\n .slice(0, Math.max(0, this.options.navigateDocuments ?? 2));\n\n const navigated = new Map<string, Passage[]>();\n let navigations: DocumentNavigation[] = [];\n if (toNavigate.length > 0) {\n const result = await this.navigator.navigate(query, toNavigate, {\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n ...(this.options.budget !== undefined ? { budget: this.options.budget } : {}),\n });\n navigations = result.documents;\n for (const passage of result.passages) {\n const id = String(passage.metadata?.documentId ?? '');\n navigated.set(id, [...(navigated.get(id) ?? []), passage]);\n }\n for (const navigation of navigations) {\n this.options.onNavigation?.(navigation);\n }\n }\n\n const merged: Passage[] = [];\n const emitted = new Set<string>();\n for (const passage of first) {\n const id = docOf(passage);\n const replacement = navigated.get(id);\n if (replacement === undefined || replacement.length === 0) {\n merged.push(passage);\n continue;\n }\n if (!emitted.has(id)) {\n emitted.add(id);\n merged.push(...replacement);\n }\n if (this.options.keepFirstStageChunks) {\n merged.push(passage);\n }\n }\n return { passages: merged.slice(0, topK), navigations };\n }\n}\n","import type { AiToolCtx, Passage, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { documentIdOf } from '../vector-store.js';\nimport type { TreeBudget } from './llm.js';\nimport type { NavigatedNode, NavigationStep, TreeNavigationRetriever } from './navigate.js';\n\nexport interface NavigateDocumentToolOptions {\n /** Tool name the model sees. Default `navigate_document`. */\n name?: string;\n description?: string;\n /**\n * The access filter for this call, from the tool context — e.g. `(ctx) => ({ tenant: ctx.actor.tenantRef })`.\n * Applied to the tree read, with the vector stores' semantics. Omit only for single-tenant corpora.\n */\n filter?: (ctx: AiToolCtx) => Record<string, unknown> | undefined;\n /** Per-call navigation budget (overrides the navigator's). */\n budget?: TreeBudget;\n}\n\nexport interface NavigateDocumentInput {\n documentId: string;\n question: string;\n}\n\nexport interface NavigateDocumentOutput {\n documentId: string;\n /** `false` when the document has no tree (short documents don't get one) or the caller may not read it. */\n found: boolean;\n passages: Passage[];\n /** The sections chosen and the path to each. */\n nodes: NavigatedNode[];\n /** The walk, step by step, with the model's reasoning. */\n steps: NavigationStep[];\n stoppedBy?: string;\n note?: string;\n}\n\n/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */\nexport interface NavigateDocumentTool {\n spec: ToolSpec;\n handler: ToolHandler;\n}\n\nfunction isInput(value: unknown): value is NavigateDocumentInput {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Record<string, unknown>).documentId === 'string' &&\n typeof (value as Record<string, unknown>).question === 'string'\n );\n}\n\nconst inputSchema: StandardSchemaV1<NavigateDocumentInput> = {\n '~standard': {\n version: 1,\n vendor: 'nestjs-agent-rag',\n validate(value: unknown) {\n return isInput(value)\n ? { value: { documentId: value.documentId, question: value.question } }\n : {\n issues: [\n { message: 'navigate_document expects { documentId: string, question: string }' },\n ],\n };\n },\n },\n};\n\n/**\n * The agentic half of two-stage retrieval: `navigate_document(documentId, question)`, a `read`-kind\n * tool the model calls **after** `search_knowledge` put a long document among its hits — it walks\n * that document's table of contents to the sections that answer and returns their text, with the\n * path and reasoning (the citation surface: section titles and page ranges).\n *\n * `documentId` may also be a chunk id from a search result (`doc#12`); the chunk suffix is dropped.\n * A document with no tree answers `found: false` with a note, so the model falls back to the\n * passages it already has instead of retrying.\n *\n * ```ts\n * providers: [\n * provideAgentTool(createRetrievalTool(hybrid)),\n * provideAgentTool(createNavigateDocumentTool(navigator, { filter: (ctx) => ({ tenant: ctx.actor.tenantRef }) })),\n * ]\n * ```\n */\nexport function createNavigateDocumentTool(\n navigator: TreeNavigationRetriever,\n options: NavigateDocumentToolOptions = {},\n): NavigateDocumentTool {\n return {\n spec: {\n name: options.name ?? 'navigate_document',\n kind: 'read',\n description:\n options.description ??\n 'Read inside one long document (regulation, contract, filing, manual) by navigating its table of contents to the sections that answer a question. Use it after a search returned a long document: pass that document id and the question. Returns the matching sections with titles and page ranges for citation.',\n inputSchema,\n },\n handler: {\n async execute(input: unknown, ctx: AiToolCtx): Promise<NavigateDocumentOutput> {\n if (!isInput(input)) {\n throw new Error('navigate_document expects { documentId: string, question: string }');\n }\n const filter = options.filter?.(ctx);\n const navigateOptions = {\n ...(filter !== undefined ? { filter } : {}),\n ...(options.budget !== undefined ? { budget: options.budget } : {}),\n };\n let documentId = input.documentId.trim();\n let result = await navigator.navigate(input.question, [documentId], navigateOptions);\n let navigation = result.documents[0];\n if (navigation?.stoppedBy === 'not-found') {\n const bare = documentIdOf(documentId.replace(/#node:\\d+$/, ''));\n if (bare !== documentId) {\n documentId = bare;\n result = await navigator.navigate(input.question, [documentId], navigateOptions);\n navigation = result.documents[0];\n }\n }\n if (navigation === undefined || navigation.stoppedBy === 'not-found') {\n return {\n documentId,\n found: false,\n passages: [],\n nodes: [],\n steps: [],\n note: 'This document has no navigable table of contents (it is short, not indexed, or not accessible). Use the search passages instead.',\n };\n }\n return {\n documentId,\n found: true,\n passages: result.passages,\n nodes: navigation.nodes,\n steps: navigation.steps,\n ...(navigation.stoppedBy !== undefined ? { stoppedBy: navigation.stoppedBy } : {}),\n ...(navigation.nodes.length === 0\n ? { note: 'No section of this document looked relevant to the question.' }\n : {}),\n };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACsCO,SAASA,UAAUC,MAAcC,UAAwB,CAAC,GAAC;AAChE,QAAMC,YAAYD,QAAQC,aAAa;AACvC,QAAMC,WAAUF,QAAQE,WAAW;AACnC,QAAMC,aAAaJ,KAAKK,KAAI;AAC5B,MAAID,WAAWE,WAAW,GAAG;AAC3B,WAAO,CAAA;EACT;AACA,MAAIL,QAAQM,cAAcC,UAAaP,QAAQM,UAAUD,SAAS,GAAG;AACnE,WAAOG,cAAcL,YAAYF,WAAWD,QAAQM,SAAS;EAC/D;AACA,MAAIH,WAAWE,UAAUJ,WAAW;AAClC,WAAO;MAACE;;EACV;AAEA,QAAMM,SAAmB,CAAA;AACzB,MAAIC,QAAQ;AACZ,SAAOA,QAAQP,WAAWE,QAAQ;AAChC,UAAMM,MAAMC,KAAKC,IAAIH,QAAQT,WAAWE,WAAWE,MAAM;AACzD,QAAIS,UAAUH;AACd,QAAIA,MAAMR,WAAWE,QAAQ;AAC3B,YAAMU,SAASZ,WAAWa,MAAMN,OAAOC,GAAAA;AACvC,YAAMM,WAAWL,KAAKM,IACpBH,OAAOI,YAAY,MAAA,GACnBJ,OAAOI,YAAY,IAAA,GACnBJ,OAAOI,YAAY,GAAA,CAAA;AAGrB,UAAIF,WAAWhB,YAAY,GAAG;AAC5Ba,kBAAUJ,QAAQO,WAAW;MAC/B;IACF;AACA,UAAMG,QAAQjB,WAAWa,MAAMN,OAAOI,OAAAA,EAASV,KAAI;AACnD,QAAIgB,MAAMf,SAAS,GAAG;AACpBI,aAAOY,KAAKD,KAAAA;IACd;AACA,QAAIN,WAAWX,WAAWE,QAAQ;AAChC;IACF;AAEAK,YAAQE,KAAKM,IAAIJ,UAAUZ,UAASQ,QAAQ,CAAA;EAC9C;AACA,SAAOD;AACT;AA1CgBX;AAmDhB,SAASU,cAAcT,MAAcE,WAAmBK,WAAiB;AACvE,QAAMG,SAAmB,CAAA;AACzB,MAAIa,UAAU;AACd,QAAMC,QAAQ,6BAAA;AACZ,QAAID,QAAQjB,SAAS,GAAG;AACtBI,aAAOY,KAAKC,OAAAA;AACZA,gBAAU;IACZ;EACF,GALc;AAMd,aAAWE,UAAUzB,KAAK0B,MAAMnB,SAAAA,GAAY;AAC1C,UAAMoB,UAAUF,OAAOpB,KAAI;AAC3B,QAAIsB,QAAQrB,WAAW,GAAG;AACxB;IACF;AACA,QAAIqB,QAAQrB,SAASJ,WAAW;AAC9BsB,YAAAA;AAIAd,aAAOY,KAAI,GAAIvB,UAAU4B,SAAS;QAAEzB;QAAWC,SAAS;MAAE,CAAA,CAAA;AAC1D;IACF;AACA,UAAMyB,aAAYL,QAAQjB,SAAS,IAAI,GAAGiB,OAAAA,GAAUhB,SAAAA,GAAYoB,OAAAA,KAAYA;AAC5E,QAAIC,WAAUtB,SAASJ,WAAW;AAChCsB,YAAAA;AACAD,gBAAUI;IACZ,OAAO;AACLJ,gBAAUK;IACZ;EACF;AACAJ,QAAAA;AACA,SAAOd;AACT;AAhCSD;;;AC2HF,SAASoB,qBAAqBC,OAAkB;AACrD,SAAO,OAAQA,MAAsCC,eAAe;AACtE;AAFgBF;AAUT,IAAMG,qBAAN,cAAiCC,MAAAA;EAfxC,OAewCA;;;;EACtC,YAEWC,QACTC,SACA;AACA,UAAMA,OAAAA,GAAAA,KAHGD,SAAAA;AAIT,SAAKE,OAAO;EACd;AACF;AA4BO,SAASC,aAAaC,SAAe;AAC1C,QAAMC,QAAQ,UAAUC,KAAKF,OAAAA;AAC7B,SAAOC,QAAQ,CAAA,MAAOE,SAAY,IAAIC,OAAOH,MAAM,CAAA,CAAE;AACvD;AAHgBF;AAMT,SAASM,aAAaL,SAAe;AAC1C,SAAOA,QAAQM,QAAQ,SAAS,EAAA;AAClC;AAFgBD;;;ACrOT,SAASE,mBAAmBC,OAAoB;AAIrD,QAAMC,MAA+B,CAAC;AACtC,QAAMC,SAAmB,CAAA;AACzB,aAAW,CAACC,KAAKC,KAAAA,KAAUC,OAAOC,QAAQN,KAAAA,GAAQ;AAChD,QAAII,UAAU,MAAM;AAClBF,aAAOK,KAAKJ,GAAAA;IACd,WAAWC,UAAUI,QAAW;AAC9BP,UAAIE,GAAAA,IAAOC;IACb;EACF;AACA,SAAO;IAAEH;IAAKC;EAAO;AACvB;AAdgBH;AAiBT,SAASU,qBAAqBT,OAAoB;AACvD,QAAM,EAAEC,KAAKC,OAAM,IAAKH,mBAAmBC,KAAAA;AAC3C,SAAOK,OAAOK,KAAKT,GAAAA,EAAKU,WAAW,KAAKT,OAAOS,WAAW;AAC5D;AAHgBF;AAYT,SAASG,mBACdC,UACAb,OAAoB;AAEpB,QAAM,EAAEC,KAAKC,OAAM,IAAKH,mBAAmBC,KAAAA;AAC3C,QAAMc,OAAgC;IAAE,GAAGD;IAAU,GAAGZ;EAAI;AAC5D,aAAWE,OAAOD,QAAQ;AACxB,WAAOY,KAAKX,GAAAA;EACd;AACA,SAAOW;AACT;AAVgBF;;;ACtDT,SAASG,cACdC,UACAC,QAA+B;AAE/B,QAAMC,UAAUC,OAAOD,QAAQD,MAAAA;AAC/B,MAAID,aAAaI,QAAW;AAC1B,WAAOF,QAAQG,WAAW;EAC5B;AACA,SAAOH,QAAQI,MAAM,CAAC,CAACC,KAAKC,QAAAA,MAAS;AACnC,QAAIC,MAAMC,QAAQF,QAAAA,GAAW;AAC3B,YAAMG,SAASX,SAASO,GAAAA;AACxB,aAAOE,MAAMC,QAAQC,MAAAA,IACjBA,OAAOC,KAAK,CAACC,UAAUL,SAASM,SAASD,KAAAA,CAAAA,IACzCL,SAASM,SAASH,MAAAA;IACxB;AACA,WAAOX,SAASO,GAAAA,MAASC;EAC3B,CAAA;AACF;AAjBgBT;AA+BT,SAASgB,qBAAqBd,QAAgC;AACnE,MAAIA,WAAWG,QAAW;AACxB,WAAO;EACT;AACA,SAAOD,OAAOa,OAAOf,MAAAA,EAAQW,KAAK,CAACC,UAAUJ,MAAMC,QAAQG,KAAAA,KAAUA,MAAMR,WAAW,CAAA;AACxF;AALgBU;;;AChBT,IAAME,oBAAN,MAAMA;EAzBb,OAyBaA;;;EACMC,UAAU,oBAAIC,IAAAA;;;;;;EAO/BC,oBAAyC;AACvC,WAAO;MAAEC,OAAO;IAAS;EAC3B;EAEA,MAAMC,OAAOJ,SAAwC;AACnD,eAAWK,UAAUL,SAAS;AAC5B,WAAKA,QAAQM,IAAID,OAAOE,IAAIF,MAAAA;IAC9B;EACF;EAEA,MAAMG,OAAOC,YAAmC;AAC9C,eAAWF,MAAM,KAAKP,QAAQU,KAAI,GAAI;AACpC,UAAIC,aAAaJ,EAAAA,MAAQE,YAAY;AACnC,aAAKT,QAAQY,OAAOL,EAAAA;MACtB;IACF;EACF;;;;;;;EAQA,MAAMM,eAAeJ,YAAoBK,OAAuC;AAC9E,QAAIC,qBAAqBD,KAAAA,GAAQ;AAC/B,aAAO;IACT;AACA,QAAIE,UAAU;AACd,eAAW,CAACT,IAAIF,MAAAA,KAAW,KAAKL,SAAS;AACvC,UAAIW,aAAaJ,EAAAA,MAAQE,YAAY;AACnC;MACF;AACA,WAAKT,QAAQM,IAAIC,IAAI;QAAE,GAAGF;QAAQY,UAAUC,mBAAmBb,OAAOY,UAAUH,KAAAA;MAAO,CAAA;AACvFE,iBAAW;IACb;AACA,WAAOA;EACT;EAEA,MAAMG,cAAcC,QAA8D;AAChF,UAAMC,YAAY,oBAAIpB,IAAAA;AACtB,eAAWI,UAAU,KAAKL,QAAQsB,OAAM,GAAI;AAC1C,UAAIF,WAAWG,UAAa,CAACC,cAAcnB,OAAOY,UAAUG,MAAAA,GAAS;AACnE;MACF;AACA,YAAMb,KAAKI,aAAaN,OAAOE,EAAE;AACjC,UAAI,CAACc,UAAUI,IAAIlB,EAAAA,GAAK;AACtBc,kBAAUf,IAAIC,IAAI;UAChBA;UACA,GAAIF,OAAOY,aAAaM,SAAY;YAAEN,UAAUZ,OAAOY;UAAS,IAAI,CAAC;QACvE,CAAA;MACF;IACF;AACA,WAAO;SAAII,UAAUC,OAAM;;EAC7B;;;;;;;;;;EAWA,MAAMI,WAAWjB,YAAoBkB,SAAqD;AACxF,UAAMC,SAAwB,CAAA;AAC9B,eAAWvB,UAAU,KAAKL,QAAQsB,OAAM,GAAI;AAC1C,UAAIX,aAAaN,OAAOE,EAAE,MAAME,YAAY;AAC1C;MACF;AACAmB,aAAOC,KAAK;QACVtB,IAAIF,OAAOE;QACXuB,OAAOC,aAAa1B,OAAOE,EAAE;QAC7ByB,MAAM3B,OAAO2B;QACb,GAAI3B,OAAOY,aAAaM,SAAY;UAAEN,UAAUZ,OAAOY;QAAS,IAAI,CAAC;MACvE,CAAA;IACF;AACAW,WAAOK,KAAK,CAACC,GAAGC,MAAMD,EAAEJ,QAAQK,EAAEL,KAAK;AACvC,UAAMM,SAAST,SAASS,UAAU;AAClC,WAAOT,SAASU,UAAUd,SACtBK,OAAOU,MAAMF,MAAAA,IACbR,OAAOU,MAAMF,QAAQA,SAAST,QAAQU,KAAK;EACjD;EAEA,MAAME,gBAAgBnB,QAAqD;AACzE,QAAIoB,qBAAqBpB,MAAAA,GAAS;AAChC,aAAO,CAAA;IACT;AACA,UAAMC,YAAY,oBAAIoB,IAAAA;AACtB,eAAWpC,UAAU,KAAKL,QAAQsB,OAAM,GAAI;AAC1C,UAAIF,WAAWG,UAAa,CAACC,cAAcnB,OAAOY,UAAUG,MAAAA,GAAS;AACnE;MACF;AACAC,gBAAUqB,IAAI/B,aAAaN,OAAOE,EAAE,CAAA;IACtC;AACA,WAAO;SAAIc;;EACb;EAEA,MAAMsB,WAAWC,aAAsC;AACrD,QAAIA,YAAYC,WAAW,GAAG;AAC5B;IACF;AACA,UAAMC,SAAS,IAAIL,IAAIG,WAAAA;AACvB,eAAWrC,MAAM,KAAKP,QAAQU,KAAI,GAAI;AACpC,UAAIoC,OAAOrB,IAAId,aAAaJ,EAAAA,CAAAA,GAAM;AAChC,aAAKP,QAAQY,OAAOL,EAAAA;MACtB;IACF;EACF;;;;;;;EAQA,MAAMwC,YAAY3B,QAAkD;AAClE,QAAI4B,OAAOtC,KAAKU,MAAAA,EAAQyB,WAAW,GAAG;AACpC,YAAM,IAAII,mBACR,gBACA,gMAEE;IAEN;AACA,QAAIT,qBAAqBpB,MAAAA,GAAS;AAChC,aAAO;IACT;AACA,QAAI8B,UAAU;AACd,eAAW,CAAC3C,IAAIF,MAAAA,KAAW,KAAKL,SAAS;AACvC,UAAIwB,cAAcnB,OAAOY,UAAUG,MAAAA,GAAS;AAC1C,aAAKpB,QAAQY,OAAOL,EAAAA;AACpB2C,mBAAW;MACb;IACF;AACA,WAAOA;EACT;EAEA,MAAMC,YAAY/B,QAAmD;AACnE,QAAIoB,qBAAqBpB,MAAAA,GAAS;AAChC,aAAO;IACT;AACA,QAAIgC,QAAQ;AACZ,eAAW/C,UAAU,KAAKL,QAAQsB,OAAM,GAAI;AAC1C,UAAIF,WAAWG,UAAaC,cAAcnB,OAAOY,UAAUG,MAAAA,GAAS;AAClEgC,iBAAS;MACX;IACF;AACA,WAAOA;EACT;EAEA,MAAMC,OAAOC,WAAqB3B,SAAkD;AAClF,UAAM4B,SAAoB,CAAA;AAC1B,eAAWlD,UAAU,KAAKL,QAAQsB,OAAM,GAAI;AAC1C,UAAIK,QAAQP,WAAWG,UAAa,CAACC,cAAcnB,OAAOY,UAAUU,QAAQP,MAAM,GAAG;AACnF;MACF;AACAmC,aAAO1B,KAAK;QACVtB,IAAIF,OAAOE;QACXyB,MAAM3B,OAAO2B;QACbwB,OAAOC,iBAAiBH,WAAWjD,OAAOiD,SAAS;QACnD,GAAIjD,OAAOqD,WAAWnC,SAAY;UAAEmC,QAAQrD,OAAOqD;QAAO,IAAI,CAAC;QAC/D,GAAIrD,OAAOY,aAAaM,SAAY;UAAEN,UAAUZ,OAAOY;QAAS,IAAI,CAAC;MACvE,CAAA;IACF;AACA,WAAOsC,OAAOtB,KAAK,CAACC,GAAGC,MAAMA,EAAEqB,QAAQtB,EAAEsB,KAAK,EAAElB,MAAM,GAAGX,QAAQgC,IAAI;EACvE;AACF;AAEA,SAASF,iBAAiBvB,GAAaC,GAAW;AAChD,MAAIyB,MAAM;AACV,MAAIC,QAAQ;AACZ,MAAIC,QAAQ;AACZ,QAAMjB,SAASkB,KAAKC,IAAI9B,EAAEW,QAAQV,EAAEU,MAAM;AAC1C,WAASf,QAAQ,GAAGA,QAAQe,QAAQf,SAAS,GAAG;AAC9C,UAAMmC,SAAS/B,EAAEJ,KAAAA,KAAU;AAC3B,UAAMoC,SAAS/B,EAAEL,KAAAA,KAAU;AAC3B8B,WAAOK,SAASC;AAChBL,aAASI,SAASA;AAClBH,aAASI,SAASA;EACpB;AACA,QAAMC,cAAcJ,KAAKK,KAAKP,KAAAA,IAASE,KAAKK,KAAKN,KAAAA;AACjD,SAAOK,gBAAgB,IAAI,IAAIP,MAAMO;AACvC;AAdSV;;;ACjMT,IAAMY,QAAQ,wBAACC,MAAc,IAAIC,IAAID,EAAEE,MAAM,KAAA,EAAOC,OAAOC,OAAAA,CAAAA,GAA7C;AAOP,IAAMC,qBAAoE;EAC/EC,SAASP,MAAM;;;;;;;uDAOsC;EACrDQ,YAAYR,MAAM;;;;;;;;;;;;;;uBAcA;EAClBS,SAAST,MAAM;;;;;;;;;;;;;;;;;;;;;;oEAsBmD;AACpE;AAEA,IAAMU,QAAQ;AAQP,SAASC,aACdC,OACAC,MAAM,IACNC,YAA2DR,oBAAkB;AAE7E,QAAMS,SAAS;OAAI,IAAIb,IAAIU,MAAMI,YAAW,EAAGC,UAAU,KAAA,EAAOC,MAAMR,KAAAA,KAAU,CAAA,CAAE;;AAClF,QAAMS,SAASC,OAAOC,QAAQP,SAAAA,EAAWQ,IACvC,CAAC,CAAA,EAAGC,KAAAA,MAAU;IAACA;IAAMR,OAAOX,OAAO,CAACoB,MAAMD,MAAKE,IAAID,CAAAA,CAAAA,EAAIE;GAAO;AAEhE,QAAMC,OAAOC,KAAKf,IAAI,GAAA,GAAMM,OAAOG,IAAI,CAAC,CAAA,EAAGO,CAAAA,MAAOA,CAAAA,CAAAA;AAClD,MAAIF,SAAS,EAAG,QAAOZ,OAAOe,MAAM,GAAGjB,GAAAA;AACvC,QAAMU,OAAOJ,OAAOf,OAAO,CAAC,CAAA,EAAGyB,CAAAA,MAAOA,MAAMF,IAAAA,EAAML,IAAI,CAAC,CAACrB,CAAAA,MAAOA,CAAAA;AAC/D,QAAM8B,SAAQhB,OAAOX,OAAO,CAACoB,MAAM,CAACD,KAAKS,KAAK,CAAC/B,MAAMA,EAAEwB,IAAID,CAAAA,CAAAA,CAAAA;AAC3D,UAAQO,OAAML,SAAS,IAAIK,SAAQhB,QAAQe,MAAM,GAAGjB,GAAAA;AACtD;AAdgBF;AAiBT,SAASsB,gBAAgBrB,OAAa;AAC3C,SAAO,UAAUsB,KAAKtB,KAAAA,KAAU,uBAAuBsB,KAAKtB,KAAAA;AAC9D;AAFgBqB;AAKT,SAASE,eAAeJ,QAAe;AAC5C,SAAOA,OAAMT,IAAI,CAACE,MAAM,IAAIA,EAAEY,QAAQ,MAAM,EAAA,CAAA,GAAM,EAAEC,KAAK,KAAA;AAC3D;AAFgBF;;;ACtDhB,IAAMG,UAAUC,uBAAOC,IAAI,sCAAA;AAepB,SAASC,WAAWC,QAAsB;AAC/C,MAAKA,OAAgCJ,OAAAA,GAAU;AAC7C,WAAOI;EACT;AACA,MAAIC,UAAUD,MAAAA,GAAS;AACrB,WAAOD,WAAWC,OAAOE,OAAO;EAClC;AACA,MAAI,OAAQF,OAAyBG,WAAW,YAAY;AAC1D,WAAOC,eAAeJ,MAAAA;EACxB;AACA,SAAOK,cAAcL,MAAAA;AACvB;AAXgBD;AAahB,SAASE,UAAUD,QAAsB;AACvC,QAAMM,SAAUN,OAAgCE;AAChD,SAAOI,WAAW,QAAQA,WAAWC,UAAa,OAAOD,WAAW;AACtE;AAHSL;AAKT,SAASO,KAAKF,QAAgB;AAC5BG,SAAOC,eAAeJ,QAAQV,SAAS;IAAEe,OAAO;EAAK,CAAA;AACrD,SAAOL;AACT;AAHSE;AAKT,SAASI,OAAYC,QAAe;AAClC,MAAIC,MAAMC,QAAQF,MAAAA,GAAS;AACzB,WAAOA;EACT;AACA,QAAMG,OAAQH,QAAkDG;AAChE,SAAOF,MAAMC,QAAQC,IAAAA,IAASA,OAAiB,CAAA;AACjD;AANSJ;AAQT,SAASR,eAAea,KAAkB;AACxC,QAAMX,SAAmB;IACvBY,OAAO,8BAAYC,MAAcC,SAAoB,CAAA;;MAEnD;WAAIR,OAAY,MAAMK,IAAId,OAAOgB,MAAMC,MAAAA,CAAAA;;OAFlC;EAGT;AACA,MAAI,OAAOH,IAAII,UAAU,YAAY;AACnC,UAAMA,QAAQJ,IAAII,MAAMC,KAAKL,GAAAA;AAC7BX,WAAOiB,cAAc,CAAIC,SACvBC,QAAQC,QAAQL,MAAM,CAACM,OAAsBH,KAAKpB,eAAeuB,EAAAA,CAAAA,CAAAA,CAAAA;EACrE;AACA,SAAOnB,KAAKF,MAAAA;AACd;AAZSF;AAmBT,SAASC,cAAcL,QAA2C;AAChE,QAAMM,SAAmB;IACvBY,OAAO,8BAAYC,MAAcC,WAC/BR,OAAY,MAAMZ,OAAOkB,MAAMC,MAAMC,MAAAA,CAAAA,GADhC;EAET;AACA,MAAI,OAAOpB,OAAOuB,gBAAgB,YAAY;AAE5C,UAAMA,cAAcvB,OAAOuB,YAAYD,KAAKtB,MAAAA;AAC5CM,WAAOiB,cAAc,CAAIC,SACvBD,YAAY,CAACI,OAAiBH,KAAKzB,WAAW4B,EAAAA,CAAAA,CAAAA;EAClD,WAAWC,aAAa5B,MAAAA,GAAS;AAC/BM,WAAOiB,cAAc,OAAUC,SAAAA;AAC7B,YAAMK,aAAa,MAAM7B,OAAO8B,QAAO;AACvC,UAAIC,SAAS;AACb,UAAI;AACF,eAAO,MAAMC,iBAAiBH,YAAYL,MAAM,MAAA;AAC9CO,mBAAS;QACX,CAAA;MACF,UAAA;AAGEF,mBAAWI,QAAQF,SAAS,IAAIG,MAAM,iBAAA,IAAqB3B,MAAAA;MAC7D;IACF;EACF,WAAW4B,eAAenC,MAAAA,GAAS;AACjCM,WAAOiB,cAAc,CAAIC,SAAuCQ,iBAAiBhC,QAAQwB,IAAAA;EAC3F;AACA,SAAOhB,KAAKF,MAAAA;AACd;AA5BSD;AA8BT,eAAe2B,iBACbH,YACAL,MACAY,kBAA6B;AAE7B,QAAMT,KAAKnB,KAAK;IACdU,OAAO,8BAAYC,MAAcC,WAC/BR,OAAY,MAAMiB,WAAWX,MAAMC,MAAMC,MAAAA,CAAAA,GADpC;EAET,CAAA;AACA,QAAMS,WAAWX,MAAM,OAAA;AACvB,MAAI;AACF,UAAML,SAAS,MAAMW,KAAKG,EAAAA;AAC1B,UAAME,WAAWX,MAAM,QAAA;AACvB,WAAOL;EACT,SAASwB,OAAO;AAEd,UAAMR,WAAWX,MAAM,UAAA,EAAYoB,MAAM,MAAMF,mBAAAA,CAAAA;AAC/C,UAAMC;EACR;AACF;AAnBeL;AAqBf,SAASJ,aAAa5B,QAAc;AAClC,SACE,OAAQA,OAAmC8B,YAAY,cACvD,OAAQ9B,OAAmCuC,eAAe;AAE9D;AALSX;AAQT,SAASO,eAAenC,QAAc;AACpC,SACE,OAAQA,OAAiCiC,YAAY,cACrD,OAAQjC,OAAmCwC,cAAc,YACxDxC,OAA8CyC,yBAAyBlC;AAE5E;AANS4B;;;ACpET,IAAMO,WAAWC,OAAOC,aAAa,CAAA;AAe9B,SAASC,cAAiBC,OAAQ;AACvC,MAAI,OAAOA,UAAU,UAAU;AAC7B,WAAQA,MAAMC,SAASL,QAAAA,IAAYI,MAAME,MAAMN,QAAAA,EAAUO,KAAK,EAAA,IAAMH;EACtE;AACA,MAAII,MAAMC,QAAQL,KAAAA,GAAQ;AACxB,WAAOA,MAAMM,IAAI,CAACC,SAASR,cAAcQ,IAAAA,CAAAA;EAC3C;AACA,MAAIP,UAAU,QAAQ,OAAOA,UAAU,UAAU;AAC/C,UAAMQ,QAAQC,OAAOC,eAAeV,KAAAA;AACpC,QAAIQ,UAAUC,OAAOE,aAAaH,UAAU,MAAM;AAChD,aAAOR;IACT;AACA,WAAOS,OAAOG,YACZH,OAAOI,QAAQb,KAAAA,EAAkCM,IAAI,CAAC,CAACQ,KAAKC,KAAAA,MAAW;MACrEhB,cAAce,GAAAA;MACdf,cAAcgB,KAAAA;KACf,CAAA;EAEL;AACA,SAAOf;AACT;AApBgBD;AAsDT,IAAMiB,gBAAN,MAAMA;EAvKb,OAuKaA;;;EACQC;EACAC;;EAEFC;;EAEAC;EACAC;EACAC;EACAC;EACAC;EACTC;EAER,YAAYR,QAAwBS,UAAgC,CAAC,GAAG;AACtE,SAAKT,SAASU,WAAWV,MAAAA;AACzB,SAAKC,QAAQQ,QAAQR,SAAS;AAC9B,UAAMC,aAAaO,QAAQP,cAAc;AACzC,QAAI,OAAOA,eAAe,UAAU;AAClC,WAAKA,aAAaA;AAClB,WAAKC,oBAAoB,CAAA;IAC3B,OAAO;AACL,iBAAWQ,SAAST,YAAY;AAC9B,YAAI,CAACU,OAAOC,UAAUF,KAAAA,KAAUA,SAAS,GAAG;AAC1C,gBAAM,IAAIG,MAAM,0CAA0ClC,OAAO+B,KAAAA,CAAAA,EAAQ;QAC3E;MACF;AACA,WAAKT,aAAaa;AAClB,WAAKZ,oBAAoB;WAAI,IAAIa,IAAId,UAAAA;;IACvC;AACA,SAAKE,qBAAqBK,QAAQL,sBAAsB;AACxD,SAAKC,gBAAgBI,QAAQJ;AAC7B,SAAKC,WAAWG,QAAQH;AACxB,SAAKC,kBAAkBU,KAAKC,IAAI,GAAGD,KAAKE,MAAMV,QAAQF,mBAAmB,GAAA,CAAA;EAC3E;;EAGAa,oBAAyC;AACvC,WAAO;MAAEC,OAAO;MAAMC,YAAY,KAAKrB;IAAM;EAC/C;;;;;EAMAsB,mBAA6B;AAC3B,UAAMC,QAAQ,KAAKtB,eAAea;AAClC,UAAMU,SAASD,QAAQ,WAAW,UAAU,KAAKtB,UAAU;AAC3D,UAAMwB,aAAa;MACjB;MACA,8BAA8B,KAAKzB,KAAK;;;;;oBAK1BwB,MAAAA,GAAS,KAAKrB,qBAAqB,KAAK,WAAA;;;AAGxD,QAAIoB,OAAO;AACT,iBAAWb,SAAS,KAAKR,mBAAmB;AAC1CuB,mBAAWC,KACT,8BAA8B,KAAK1B,KAAK,cAAcU,KAAAA;aACnD,KAAKV,KAAK,mCAAmCU,KAAAA;yCACjBA,KAAAA,EAAO;MAE1C;IACF,OAAO;AACLe,iBAAWC,KACT,8BAA8B,KAAK1B,KAAK;aACnC,KAAKA,KAAK,2CAA2C;IAE9D;AACA,WAAOyB;EACT;;EAGA,MAAME,eAA8B;AAClC,eAAWC,aAAa,KAAKN,iBAAgB,GAAI;AAC/C,YAAM,KAAKvB,OAAO8B,MAAMD,SAAAA;IAC1B;EACF;;;;;;EAOA,MAAME,OAAOC,SAAwC;AAGnD,UAAMC,OAAO,oBAAIC,IAAAA;AACjB,eAAWC,UAAUH,SAAS;AAC5B,YAAMI,KAAKtD,cAAcqD,OAAOC,EAAE;AAElCH,WAAKI,OAAOD,EAAAA;AACZH,WAAKK,IAAIF,IAAI;QAAE,GAAGD;QAAQC;MAAG,CAAA;IAC/B;AAGA,UAAMG,SAAS;SAAIN,KAAKO,OAAM;;AAC9B,aAASC,QAAQ,GAAGA,QAAQF,OAAOG,QAAQD,SAAS,KAAKlC,iBAAiB;AACxE,YAAMoC,QAAQJ,OAAOK,MAAMH,OAAOA,QAAQ,KAAKlC,eAAe;AAC9D,YAAMsC,SAAoB,CAAA;AAC1B,YAAMC,OAAOH,MAAMtD,IAAI,CAAC8C,WAAAA;AACtB,cAAMY,OAAOjE,cAAcqD,OAAOY,IAAI;AACtC,cAAMC,SAASb,OAAOa,WAAWjC,SAAYjC,cAAcqD,OAAOa,MAAM,IAAIjC;AAC5E,cAAMkC,WAAWd,OAAOc,aAAalC,SAAYjC,cAAcqD,OAAOc,QAAQ,IAAIlC;AAClF,cAAMmC,OAAOL,OAAOH;AACpBG,eAAOlB,KACLQ,OAAOC,IACPW,MACAC,UAAU,MACVC,aAAalC,SAAYoC,KAAKC,UAAUH,QAAAA,IAAY,MACpDd,OAAOkB,UAAUX,SAAS,IAAIY,gBAAgBnB,OAAOkB,SAAS,IAAI,IAAA;AAEpE,eAAO,KAAKH,OAAO,CAAA,MAAOA,OAAO,CAAA,MAAOA,OAAO,CAAA,MAAOA,OAAO,CAAA,aAAcA,OAAO,CAAA;MACpF,CAAA;AACA,YAAM,KAAKlD,OAAO8B,MAChB,eAAe,KAAK7B,KAAK;kBACf6C,KAAK5D,KAAK,IAAA,CAAA;;;;;4CAMpB2D,MAAAA;IAEJ;EACF;EAEA,MAAMU,OAAOC,YAAmC;AAG9C,UAAM,KAAKxD,OAAO8B,MAAM,eAAe,KAAK7B,KAAK,UAAUwD,sBAAAA,SAA+B;MACxF3E,cAAc0E,UAAAA;KACf;EACH;;;;;;;;;;;;;EAcA,MAAME,eAAeF,YAAoBG,OAAuC;AAC9E,QAAIC,qBAAqBD,KAAAA,GAAQ;AAC/B,aAAO;IACT;AAGA,UAAM,EAAErB,KAAKiB,OAAM,IAAKM,mBAAmB/E,cAAc6E,KAAAA,CAAAA;AACzD,UAAMb,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B,UAAU,KAAK7B,KAAK;;gBAEVwD,sBAAAA;uBAEV;MAAC3E,cAAc0E,UAAAA;MAAaL,KAAKC,UAAUd,GAAAA;MAAMiB;KAAO;AAE1D,WAAOT,KAAKJ;EACd;EAEA,MAAMoB,cAAcC,QAA8D;AAChF,UAAMlB,SAAoB,CAAA;AAC1B,UAAMmB,QAAQ,KAAKA,MAAMD,QAAQlB,MAAAA;AAEjC,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAI7B,uBAAuB2B,sBAAAA,KAA2BA,sBAAAA;cAC1C,KAAKxD,KAAK;SACf+D,KAAAA;kBACSP,sBAAAA,IACZZ,MAAAA;AAEF,WAAOC,KAAKzD,IAAI,CAAC4E,SAAS;MACxB7B,IAAI6B,IAAIC;MACR,GAAID,IAAIhB,aAAa,OAAO;QAAEA,UAAUgB,IAAIhB;MAAS,IAAI,CAAC;IAC5D,EAAA;EACF;;;;;;;;;;;;;EAcA,MAAMkB,WAAWX,YAAoB/C,SAAqD;AAExF,UAAMoC,SAAoB;MAAC/D,cAAc0E,UAAAA;;AACzC,QAAIY,MAAM,cAAcC,mBAAAA;cACd,KAAKpE,KAAK;eACTwD,sBAAAA;;AAEX,QAAIhD,SAAS6D,UAAUvD,QAAW;AAChC8B,aAAOlB,KAAKlB,QAAQ6D,KAAK;AACzBF,aAAO,WAAWvB,OAAOH,MAAM;IACjC;AACA,QAAIjC,SAAS8D,WAAWxD,QAAW;AACjC8B,aAAOlB,KAAKlB,QAAQ8D,MAAM;AAC1BH,aAAO,YAAYvB,OAAOH,MAAM;IAClC;AACA,UAAMI,OAAO,MAAM,KAAK9C,OAAO8B,MAK5BsC,KAAKvB,MAAAA;AACR,WAAOC,KAAKzD,IAAI,CAAC4E,SAAS;MACxB7B,IAAI6B,IAAI7B;MACRoC,OAAO5D,OAAOqD,IAAIQ,WAAW;MAC7B1B,MAAMkB,IAAIlB;MACV,GAAIkB,IAAIhB,aAAa,OAAO;QAAEA,UAAUgB,IAAIhB;MAAS,IAAI,CAAC;IAC5D,EAAA;EACF;;EAGA,MAAMyB,gBAAgBX,QAAqD;AACzE,QAAIY,qBAAqBZ,MAAAA,GAAS;AAChC,aAAO,CAAA;IACT;AACA,UAAMlB,SAAoB,CAAA;AAC1B,UAAMmB,QAAQ,KAAKA,MAAMD,QAAQlB,MAAAA;AACjC,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B,mBAAmB2B,sBAAAA;cACX,KAAKxD,KAAK;SACf+D,KAAAA;yBAEHnB,MAAAA;AAEF,WAAOC,KAAKzD,IAAI,CAAC4E,QAAQA,IAAIC,MAAM;EACrC;;EAGA,MAAMU,WAAWC,aAAsC;AACrD,QAAIA,YAAYnC,WAAW,GAAG;AAC5B;IACF;AAEA,UAAM,KAAK1C,OAAO8B,MAChB,eAAe,KAAK7B,KAAK,UAAUwD,sBAAAA,sBACnC;MAAC3E,cAAc+F,WAAAA;KAAa;EAEhC;;;;;;EAOA,MAAMC,YAAYf,QAAkD;AAClE,QAAIvE,OAAOuF,KAAKhB,MAAAA,EAAQrB,WAAW,GAAG;AACpC,YAAM,IAAIsC,mBACR,gBACA,gMAEE;IAEN;AAGA,QAAIL,qBAAqBZ,MAAAA,GAAS;AAChC,aAAO;IACT;AACA,UAAMlB,SAAoB,CAAA;AAC1B,UAAMmB,QAAQ,KAAKA,MAAMD,QAAQlB,MAAAA;AACjC,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B,eAAe,KAAK7B,KAAK,IAAI+D,KAAAA,iBAC7BnB,MAAAA;AAEF,WAAOC,KAAKJ;EACd;EAEA,MAAMuC,YAAYlB,QAAmD;AACnE,QAAIY,qBAAqBZ,MAAAA,GAAS;AAChC,aAAO;IACT;AACA,UAAMlB,SAAoB,CAAA;AAC1B,UAAMmB,QAAQ,KAAKA,MAAMD,QAAQlB,MAAAA;AACjC,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B,sCAAsC,KAAK7B,KAAK,IAAI+D,KAAAA,IACpDnB,MAAAA;AAEF,WAAOjC,OAAOkC,KAAK,CAAA,GAAIoC,SAAS,CAAA;EAClC;;;;;;EAOA,MAAMC,OAAO9B,WAAqB5C,SAAkD;AAClF,QAAI4C,UAAUX,WAAW,GAAG;AAC1B,aAAO,CAAA;IACT;AAEA,UAAMG,SAAoB;MAACS,gBAAgBD,SAAAA;MAAY5C,QAAQ2E;;AAC/D,UAAMC,aAAa,KAAKC,gBAAgB7E,QAAQsD,QAAQlB,MAAAA;AACxD,QAAI0C,aAAa;AACjB,QAAI,KAAKrF,eAAea,QAAW;AACjC,YAAMJ,QAAQ0C,UAAUX;AACxB,UAAI,KAAKvC,kBAAkBnB,SAAS2B,KAAAA,GAAQ;AAC1C4E,qBAAa,UAAU5E,KAAAA;MACzB;AACA0E,iBAAW1D,KAAK,yBAAyB,4BAA4BhB,KAAAA,EAAO;IAC9E,WAAW,KAAKP,oBAAoB;AAClCiF,iBAAW1D,KAAK,uBAAA;IAClB;AAGA,UAAMF,SAAS8D,eAAe,WAAW,cAAc,eAAeA,UAAAA;AACtE,UAAMnB,MAAM,2CAA2C3C,MAAAA,YAAkB8D,UAAAA;cAC/D,KAAKtF,KAAK;SACfoF,WAAW3C,SAAS,IAAI,SAAS2C,WAAWnG,KAAK,OAAA,CAAA,KAAa,EAAA;kBACrDuC,MAAAA,YAAkB8D,UAAAA;;AAEhC,UAAMzC,OAAO,MAAM,KAAK0C,mBAAmB,CAACxF,WAAWA,OAAO8B,MAAasC,KAAKvB,MAAAA,CAAAA;AAChF,WAAOC,KAAKzD,IAAIoG,SAAAA;EAClB;;;;;;;EAQUH,gBACRvB,QACAlB,QACU;AACV,WAAO6C,WAAW3B,QAAQlB,MAAAA;EAC5B;EAEQmB,MAAMD,QAA6ClB,QAA2B;AACpF,UAAMwC,aAAa,KAAKC,gBAAgBvB,QAAQlB,MAAAA;AAChD,WAAOwC,WAAW3C,SAAS,IAAI,SAAS2C,WAAWnG,KAAK,OAAA,CAAA,KAAa;EACvE;;EAGA,MAAcsG,mBAAsBG,MAAoD;AACtF,UAAMC,cAAc,KAAK5F,OAAO4F,aAAaC,KAAK,KAAK7F,MAAM;AAC7D,QAAI4F,gBAAgB7E,QAAW;AAC7B,aAAO4E,KAAK,KAAK3F,MAAM;IACzB;AACA,UAAM8F,YAAY,KAAKzF,kBAAkBU,UAAc,MAAM,KAAKgF,sBAAqB;AACvF,QAAI,CAACD,aAAa,KAAKxF,aAAaS,QAAW;AAC7C,aAAO4E,KAAK,KAAK3F,MAAM;IACzB;AACA,WAAO4F,YAAY,OAAOI,OAAAA;AACxB,UAAIF,WAAW;AACb,cAAME,GAAGlE,MAAM,sDAAsD;UAAC,KAAKzB;SAAc;MAC3F;AACA,UAAI,KAAKC,aAAaS,QAAW;AAC/B,cAAMiF,GAAGlE,MAAM,iDAAiD;UAAClD,OAAO,KAAK0B,QAAQ;SAAE;MACzF;AACA,aAAOqF,KAAKK,EAAAA;IACd,CAAA;EACF;;EAGQD,wBAA0C;AAChD,SAAKvF,yBAAyB,KAAKR,OAChC8B,MAA8B,8DAA8D,EAC5FmE,KAAK,CAACnD,SAAAA;AACL,YAAM,CAACoD,QAAQ,GAAGC,QAAQ,CAAC,KAAKrD,KAAK,CAAA,GAAIsD,cAAc,OAAOnH,MAAM,GAAA,EAAKI,IAAIuB,MAAAA;AAC7E,aAAOsF,QAAQ,KAAKC,SAAS;IAC/B,CAAA,EACCE,MAAM,MAAM,KAAA;AACf,WAAO,KAAK7F;EACd;AACF;AAiBO,IAAM8F,uBAAN,cAAmCvG,cAAAA;EArjB1C,OAqjB0CA;;;EACvBwG;EACAC;EACAC;EACAC;EAEjB,YACE1G,QACAS,UAAmE,CAAC,GACpE;AACA,UAAMT,QAAQS,OAAAA;AACd,UAAMkG,SAASlG,QAAQmG,UAAUD,UAAU;AAC3C,QAAI,CAACE,eAAeC,KAAKH,MAAAA,GAAS;AAChC,YAAM,IAAI7F,MAAM,4DAA4D6F,MAAAA,GAAS;IACvF;AACA,UAAMlF,SAAShB,QAAQmG,UAAUnF;AACjC,QAAIA,WAAWV,UAAa,CAAC8F,eAAeC,KAAKrF,MAAAA,GAAS;AACxD,YAAM,IAAIX,MAAM,kDAAkDW,MAAAA,GAAS;IAC7E;AACA,SAAK8E,WAAWI;AAChB,SAAKH,WAAW/E;AAChB,SAAKgF,kBAAkBhG,QAAQmG,UAAUH,mBAAmB;AAC5D,UAAMC,YAAYjG,QAAQmG,UAAUF;AACpC,SAAKA,YAAYA,cAAc,QAAQ,CAAC,IAAKA,aAAaK;EAC5D;EAESxF,mBAA6B;AACpC,UAAMG,aAAa,MAAMH,iBAAAA;AACzB,QAAI,KAAKiF,aAAazF,QAAW;AAC/BW,iBAAWC,KACT,8BAA8B,KAAK1B,KAAK;aACnC,KAAKA,KAAK,eAAe,KAAK+G,SAAQ,CAAA,GAAK;IAEpD;AACA,WAAOtF;EACT;EAEA,MAAMuF,WAAWnF,OAAerB,SAAkD;AAChF,QAAIkE,qBAAqBlE,QAAQsD,MAAM,KAAKjC,MAAMoF,KAAI,MAAO,IAAI;AAC/D,aAAO,CAAA;IACT;AACA,QAAI,CAAC,KAAKT,mBAAmBU,gBAAgBrF,KAAAA,GAAQ;AACnD,YAAMsF,SAAS,MAAM,KAAKC,cAAcvF,OAAOrB,OAAAA;AAC/C,UAAI2G,OAAO1E,SAAS,KAAK,CAAC,KAAK+D,iBAAiB;AAC9C,eAAOW;MACT;IACF;AACA,UAAME,SAAQC,aAAazF,OAAO,IAAI,KAAK4E,SAAS;AACpD,WAAOY,OAAM5E,WAAW,IAAI,CAAA,IAAK,KAAK8E,oBAAoBF,QAAO7G,OAAAA;EACnE;;EAGA,MAAc4G,cAAcvF,OAAerB,SAAkD;AAC3F,UAAMgH,UAAU,yBAAyB,KAAKlB,QAAQ;AAEtD,UAAM1D,SAAoB;MAACf;MAAOrB,QAAQ2E;;AAC1C,UAAMC,aAAa;MACjB,GAAG,KAAK2B,SAAQ,CAAA,OAASS,OAAAA;SACtB,KAAKnC,gBAAgB7E,QAAQsD,QAAQlB,MAAAA;;AAE1C,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B,iDAAiD,KAAKkF,SAAQ,CAAA,KAAOS,OAAAA;cAC7D,KAAKxH,KAAK;eACToF,WAAWnG,KAAK,OAAA,CAAA;;kBAGzB2D,MAAAA;AAEF,WAAOC,KAAKzD,IAAIoG,SAAAA;EAClB;;;;;;;EAQA,MAAc+B,oBACZF,QACA7G,SACoB;AACpB,UAAMiH,MAAM,eAAe,KAAKnB,QAAQ;AAGxC,UAAM1D,SAAoB;MAAC8E,eAAeL,MAAAA;MAAQ7G,QAAQ2E;MAAMkC;;AAChE,UAAMjC,aAAa;MACjB,GAAG,KAAK2B,SAAQ,CAAA,OAASU,GAAAA;SACtB,KAAKpC,gBAAgB7E,QAAQsD,QAAQlB,MAAAA;;AAE1C,UAAMC,OAAO,MAAM,KAAK9C,OAAO8B,MAC7B;uCACiC,KAAKyE,QAAQ;;;8CAGN,KAAKS,SAAQ,CAAA;gBAC3C,KAAK/G,KAAK;iBACToF,WAAWnG,KAAK,OAAA,CAAA;;;;;;;;;;;sDAWqBwI,GAAAA;;;4DAGMA,GAAAA;kBAEtD7E,MAAAA;AAEF,WAAOC,KAAKzD,IAAIoG,SAAAA;EAClB;;EAGQuB,WAAmB;AACzB,WAAO,KAAKR,YAAY,gBAAgB,KAAKD,QAAQ;EACvD;AACF;AAEA,IAAMM,iBAAiB;AAUvB,SAASpB,UAAUxB,KAAU;AAC3B,SAAO;IACL7B,IAAI6B,IAAI7B;IACRW,MAAMkB,IAAIlB;IACV6E,OAAOhH,OAAOqD,IAAI2D,KAAK;IACvB,GAAI3D,IAAIjB,WAAW,OAAO;MAAEA,QAAQiB,IAAIjB;IAAO,IAAI,CAAC;IACpD,GAAIiB,IAAIhB,aAAa,OAAO;MAAEA,UAAUgB,IAAIhB;IAAS,IAAI,CAAC;EAC5D;AACF;AARSwC;AAuBF,SAASC,WACd3B,QACAlB,QAAiB;AAEjB,MAAIkB,WAAWhD,UAAavB,OAAOuF,KAAKhB,MAAAA,EAAQrB,WAAW,GAAG;AAC5D,WAAO,CAAA;EACT;AACA,QAAMmF,WAAW/I,cAAciF,MAAAA;AAC/B,QAAM+D,UAAoB,CAAA;AAC1B,QAAMC,SAAkC,CAAC;AACzC,aAAW,CAAClI,KAAKd,KAAAA,KAAUS,OAAOI,QAAQiI,QAAAA,GAAW;AACnD,QAAI1I,MAAMC,QAAQL,KAAAA,GAAQ;AACxB,UAAIA,MAAM2D,WAAW,GAAG;AACtBoF,gBAAQnG,KAAK,OAAA;AACb;MACF;AACAkB,aAAOlB,KAAK9B,GAAAA;AACZ,YAAMmI,WAAW,IAAInF,OAAOH,MAAM;AAClCG,aAAOlB,KAAK5C,MAAMM,IAAIT,MAAAA,CAAAA;AACtB,YAAMqJ,WAAW,IAAIpF,OAAOH,MAAM;AAClCoF,cAAQnG,KACN,qCAAqCqG,QAAAA,8BACjBA,QAAAA,qCAA6CA,QAAAA,aAAqBC,QAAAA,UAAkB;IAE5G,OAAO;AACLF,aAAOlI,GAAAA,IAAOd;IAChB;EACF;AACA,MAAIS,OAAOuF,KAAKgD,MAAAA,EAAQrF,SAAS,GAAG;AAClCG,WAAOlB,KAAKwB,KAAKC,UAAU2E,MAAAA,CAAAA;AAC3BD,YAAQnG,KAAK,gBAAgBkB,OAAOH,MAAM,SAAS;EACrD;AACA,SAAOoF;AACT;AAjCgBpC;AAwChB,IAAMjC,yBAAyB;AAG/B,IAAMY,sBAAsB;AAG5B,SAASf,gBAAgBD,WAAmB;AAC1C,SAAO,IAAIA,UAAUnE,KAAK,GAAA,CAAA;AAC5B;AAFSoE;;;AC3tBF,IAAM4E,iCAAN,cAA6CC,MAAAA;EApCpD,OAoCoDA;;;;;;;EAClD,YAEWC,OAEAC,OAEAC,UAEAC,QACTC,QACA;AACA,UACE,qBAAqBJ,KAAAA,kEACTC,KAAAA,QAAaE,MAAAA,cAAoBD,QAAAA,KAAaE,MAAAA,EAAQ,GAAA,KAX3DJ,QAAAA,OAAAA,KAEAC,QAAAA,OAAAA,KAEAC,WAAAA,UAAAA,KAEAC,SAAAA;AAOT,SAAKE,OAAO;EACd;AACF;AAgCO,IAAMC,mBAAN,MAAMA;EAtFb,OAsFaA;;;;EACMN;EACAO;EACAC;EACAC;EACAC;EAEjB,YACmBC,QACjBC,UAAmC,CAAC,GACpC;SAFiBD,SAAAA;AAGjB,SAAKX,QAAQY,QAAQZ,SAAS;AAC9B,SAAKO,SAASK,QAAQL,UAAU;AAChC,SAAKC,aAAaI,QAAQJ,cAAc;AACxC,SAAKC,mBAAmBG,QAAQH,oBAAoB,CAAA;AACpD,SAAKC,gBAAgBE,QAAQF,iBAAiB;EAChD;;;;;;EAOAG,oBAAyC;AACvC,WAAO;MAAEC,OAAO;MAASC,YAAY,KAAKf;IAAM;EAClD;;;;;;;;;;;;;;;;;;;;;;;;EAyBA,MAAMgB,eAA8B;AAClC,QAAIC;AACJ,QAAI;AACFA,aAAO,MAAM,KAAKN,OAAOO,YAAY;QAAC;QAAW,KAAKlB;OAAM;IAC9D,QAAQ;AACN,YAAM,KAAKmB,YAAW;AACtB;IACF;AACA,UAAM,KAAKC,gBAAgBH,IAAAA;EAC7B;;EAGA,MAAcG,gBAAgBH,MAA8B;AAC1D,UAAMI,aAAaC,qBAAqBL,IAAAA;AACxC,QAAII,WAAWE,SAAS,GAAG;AACzB;IACF;AAEA,UAAMC,SAASH,WAAWI,IAAI,WAAA;AAC9B,QAAID,QAAQhB,eAAekB,UAAaF,OAAOhB,eAAe,KAAKA,YAAY;AAC7E,YAAM,IAAIV,+BACR,KAAKE,OACL,aACA,OAAO,KAAKQ,UAAU,IACtB,OAAOgB,OAAOhB,UAAU,IACxB,6JAA6J,KAAKR,KAAK,kDAAkD;IAE7N;AAEA,UAAM2B,UAAoB,CAAA;AAC1B,eAAW1B,SAAS,KAAKQ,kBAAkB;AACzC,YAAMJ,OAAO,QAAQJ,KAAAA;AACrB,YAAM2B,WAAWP,WAAWI,IAAIpB,IAAAA;AAChC,UAAIuB,aAAaF,QAAW;AAC1BC,gBAAQE,KAAKxB,IAAAA;MACf,WAAWuB,SAASE,SAAS,OAAO;AAClC,cAAM,IAAIhC,+BACR,KAAKE,OACLK,MACA,OACAuB,SAASE,MACT,2EAA2E,KAAK9B,KAAK,iDAAiD;MAE1I;IACF;AACA,eAAWK,QAAQsB,SAAS;AAC1B,YAAM,KAAKhB,OAAOO,YAAY;QAAC;QAAY,KAAKlB;QAAO;QAAU;QAAOK;QAAM;OAAM;IACtF;EACF;EAEA,MAAcc,cAA6B;AACzC,UAAMY,SAAmB;MAAC;MAAQ;MAAQ;MAAU;MAAO;MAAiB;MAAQ;;AACpF,eAAW9B,SAAS,KAAKQ,kBAAkB;AACzCsB,aAAOF,KAAK,QAAQ5B,KAAAA,IAAS,KAAA;IAC/B;AACA8B,WAAOF,KACL,aACA,UACA,QACA,KACA,QACA,WACA,OACAG,OAAO,KAAKxB,UAAU,GACtB,mBACA,QAAA;AAEF,UAAM,KAAKG,OAAOO,YAAY;MAC5B;MACA,KAAKlB;MACL;MACA;MACA;MACA;MACA,KAAKO;MACL;SACGwB;KACJ;EACH;EAEA,MAAME,OAAOC,SAAwC;AACnD,eAAWC,UAAUD,SAAS;AAC5B,YAAME,OAA4B;QAChC;QACA,GAAG,KAAK7B,MAAM,GAAG4B,OAAOE,EAAE;QAC1B;QACAF,OAAOG;QACP;QACAC,aAAaJ,OAAOK,SAAS;;AAE/B,UAAIL,OAAOM,WAAWf,QAAW;AAC/BU,aAAKP,KAAK,UAAUM,OAAOM,MAAM;MACnC;AACA,UAAIN,OAAOO,aAAahB,QAAW;AACjCU,aAAKP,KAAK,iBAAiBc,KAAKC,UAAUT,OAAOO,QAAQ,CAAA;AACzD,mBAAWzC,SAAS,KAAKQ,kBAAkB;AACzC,gBAAMoC,QAAQV,OAAOO,SAASzC,KAAAA;AAC9B,cAAI4C,UAAUnB,QAAW;AACvBU,iBAAKP,KAAK,QAAQ5B,KAAAA,IAAS6C,UAAUD,KAAAA,CAAAA;UACvC;QACF;MACF;AACA,YAAM,KAAKlC,OAAOO,YAAYkB,IAAAA;IAChC;EACF;;;;;;;;;;;;;;;;;;;;;EAsBA,MAAMW,OAAOC,YAAmC;AAC9C,UAAMC,OAAO,MAAM,KAAKC,UAAUF,UAAAA;AAClC,QAAIC,KAAKE,SAAS,GAAG;AACnB,YAAM,KAAKxC,OAAOO,YAAY;QAAC;WAAU+B;OAAK;IAChD;EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCA,MAAMG,eAAeJ,YAAoBK,OAAuC;AAC9E,QAAIC,qBAAqBD,KAAAA,GAAQ;AAC/B,aAAO;IACT;AACA,UAAMJ,OAAO,MAAM,KAAKC,UAAUF,UAAAA;AAClC,QAAIO,UAAU;AACd,eAAWC,OAAOP,MAAM;AACtB,YAAMQ,MAAM,MAAM,KAAK9C,OAAOO,YAAY;QAAC;QAAQsC;QAAK;OAAgB;AACxE,YAAME,UAAUC,cAAcF,QAAQ,QAAQA,QAAQ/B,SAAYA,SAAYkC,MAAMH,GAAAA,CAAAA;AACpF,YAAMI,OAAOC,mBAAmBJ,QAAQhB,UAAUW,KAAAA;AAElD,YAAMjB,OAA4B;QAAC;QAAQoB;QAAK;QAAiBb,KAAKC,UAAUiB,IAAAA;;AAChF,YAAME,QAAkB,CAAA;AACxB,iBAAW9D,SAAS,KAAKQ,kBAAkB;AACzC,cAAMoC,QAAQgB,KAAK5D,KAAAA;AACnB,YAAI4C,UAAUnB,QAAW;AACvBU,eAAKP,KAAK,QAAQ5B,KAAAA,IAAS6C,UAAUD,KAAAA,CAAAA;QACvC,WAAWa,QAAQhB,WAAWzC,KAAAA,MAAWyB,QAAW;AAClDqC,gBAAMlC,KAAK,QAAQ5B,KAAAA,EAAO;QAC5B;MACF;AACA,UAAI8D,MAAMZ,SAAS,GAAG;AACpB,cAAM,KAAKxC,OAAOO,YAAY;UAAC;UAAQsC;aAAQO;SAAM;MACvD;AACA,YAAM,KAAKpD,OAAOO,YAAYkB,IAAAA;AAC9BmB,iBAAW;IACb;AACA,WAAOA;EACT;;;;;;;EAQA,MAAcL,UAAUF,YAAuC;AAC7D,UAAMC,OAAO,oBAAIe,IAAAA;AACjB,UAAMC,OAAO,GAAG,KAAK1D,MAAM,GAAGyC,UAAAA;AAC9B,UAAMkB,SAAS,MAAM,KAAKvD,OAAOO,YAAY;MAAC;MAAU+C;KAAK;AAC7D,QAAIE,OAAOP,MAAMM,MAAAA,CAAAA,IAAW,GAAG;AAC7BjB,WAAKmB,IAAIH,IAAAA;IACX;AACA,UAAMI,UAAU,GAAG,KAAK9D,MAAM,GAAG+D,WAAWtB,UAAAA,CAAAA;AAC5C,QAAIuB,SAAS;AACb,OAAG;AACD,YAAMC,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAC1C;QACAqD;QACA;QACAF;QACA;QACA;OACD;AACD,YAAMI,OAAOC,eAAeF,KAAAA;AAC5BD,eAASE,KAAKF;AACd,iBAAWf,OAAOiB,KAAKxB,MAAM;AAC3BA,aAAKmB,IAAIZ,GAAAA;MACX;IACF,SAASe,WAAW;AACpB,WAAO;SAAItB;;EACb;;;;;;;;;;;;;;;;;;;EAoBA,MAAM0B,WAAW3B,YAAoBpC,SAAqD;AACxF,UAAMqC,OAAO,MAAM,KAAKC,UAAUF,UAAAA;AAClC,UAAM4B,UAAU3B,KACb4B,IAAI,CAACrB,SAAS;MAAEA;MAAKnB,IAAImB,IAAIsB,MAAM,KAAKvE,OAAO4C,MAAM;IAAE,EAAA,EACvD0B,IAAI,CAACE,WAAW;MAAE,GAAGA;MAAO/E,OAAOgF,aAAaD,MAAM1C,EAAE;IAAE,EAAA,EAC1D4C,KAAK,CAACC,GAAGC,MAAMD,EAAElF,QAAQmF,EAAEnF,KAAK;AAEnC,UAAMoF,SAASxE,SAASwE,UAAU;AAClC,UAAMX,OACJ7D,SAASyE,UAAU3D,SACfkD,QAAQE,MAAMM,MAAAA,IACdR,QAAQE,MAAMM,QAAQA,SAASxE,QAAQyE,KAAK;AAElD,UAAMC,SAAwB,CAAA;AAC9B,eAAWP,SAASN,MAAM;AACxB,YAAMD,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAAC;QAAS6D,MAAMvB;QAAK;QAAQ;OAAgB;AACzF,YAAM+B,SAASC,MAAMC,QAAQjB,KAAAA,IAASA,QAAQ,CAAA;AAC9C,YAAMlC,OAAOiD,OAAO,CAAA,MAAO,QAAQA,OAAO,CAAA,MAAO7D,SAAYA,SAAYkC,MAAM2B,OAAO,CAAA,CAAE;AACxF,UAAIjD,SAASZ,QAAW;AAGtB;MACF;AACA,YAAM+B,MAAM8B,OAAO,CAAA,MAAO,QAAQA,OAAO,CAAA,MAAO7D,SAAYA,SAAYkC,MAAM2B,OAAO,CAAA,CAAE;AACvFD,aAAOzD,KAAK;QACVQ,IAAI0C,MAAM1C;QACVrC,OAAO+E,MAAM/E;QACbsC;QACA,GAAGqB,cAAcF,GAAAA;MACnB,CAAA;IACF;AACA,WAAO6B;EACT;EAEA,MAAMI,cAAcC,QAA8D;AAChF,QAAIC,qBAAqBD,MAAAA,GAAS;AAChC,aAAO,CAAA;IACT;AACA,UAAME,QAAQC,YAAYH,MAAAA;AAC1B,UAAMI,YAAY,oBAAIC,IAAAA;AACtB,UAAMC,YAAY;AAClB,QAAIb,SAAS;AACb,eAAS;AAGP,YAAMZ,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAC1C;QACA,KAAKlB;QACL6F;QACA;QACA;QACA;QACA;QACA7D,OAAOoD,MAAAA;QACPpD,OAAOiE,SAAAA;QACP;QACA;OACD;AACD,YAAMC,WAAWC,iBAAiB3B,OAAO,KAAKjE,MAAM;AACpD,iBAAW6F,WAAWF,UAAU;AAC9B,cAAM7D,KAAKgE,aAAaD,QAAQ/D,EAAE;AAClC,YAAI,CAAC0D,UAAUO,IAAIjE,EAAAA,GAAK;AACtB0D,oBAAUQ,IAAIlE,IAAI;YAChBA;YACA,GAAI+D,QAAQ1D,aAAahB,SAAY;cAAEgB,UAAU0D,QAAQ1D;YAAS,IAAI,CAAC;UACzE,CAAA;QACF;MACF;AACA,UAAIwD,SAAS/C,SAAS8C,WAAW;AAC/B;MACF;AACAb,gBAAUa;IACZ;AACA,WAAO;SAAIF,UAAUS,OAAM;;EAC7B;;;;;;;;;;;;;EAcA,MAAMC,gBAAgBd,QAAqD;AACzE,QAAIC,qBAAqBD,MAAAA,GAAS;AAChC,aAAO,CAAA;IACT;AACA,UAAME,QAAQC,YAAYH,MAAAA;AAC1B,UAAMI,YAAY,oBAAI/B,IAAAA;AACtB,QAAIoB,SAAS;AACb,eAAS;AACP,YAAMZ,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAC1C;QACA,KAAKlB;QACL6F;QACA;QACA;QACA7D,OAAOoD,MAAAA;QACPpD,OAAO0E,SAAAA;QACP;QACA;OACD;AACD,YAAMzD,OAAO0D,gBAAgBnC,KAAAA;AAC7B,iBAAWhB,OAAOP,MAAM;AACtB8C,kBAAU3B,IAAIiC,aAAaO,YAAYpD,KAAK,KAAKjD,MAAM,CAAA,CAAA;MACzD;AACA,UAAI0C,KAAKE,SAASuD,WAAW;AAC3B;MACF;AACAtB,gBAAUsB;IACZ;AACA,WAAO;SAAIX;;EACb;;;;;;;;;;EAWA,MAAMc,WAAWC,aAAsC;AACrD,QAAIA,YAAY3D,WAAW,GAAG;AAC5B;IACF;AACA,UAAM4D,SAAS,IAAI/C,IAAI8C,WAAAA;AAGvB,QAAIE,UAAUF,YAAYjC,IAAI,CAACxC,OAAO,GAAG,KAAK9B,MAAM,GAAG8B,EAAAA,EAAI;AAC3D,QAAIkC,SAAS;AACb,OAAG;AACD,YAAMC,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAC1C;QACAqD;QACA;QACA,GAAGD,WAAW,KAAK/D,MAAM,CAAA;QACzB;QACAyB,OAAO0E,SAAAA;OACR;AACD,YAAMjC,OAAOC,eAAeF,KAAAA;AAC5BD,eAASE,KAAKF;AACd,iBAAWf,OAAOiB,KAAKxB,MAAM;AAC3B,YAAI8D,OAAOT,IAAID,aAAaO,YAAYpD,KAAK,KAAKjD,MAAM,CAAA,CAAA,GAAK;AAC3DyG,kBAAQnF,KAAK2B,GAAAA;QACf;MACF;AACA,UAAIwD,QAAQ7D,UAAU8D,cAAc;AAClC,cAAM,KAAKtG,OAAOO,YAAY;UAAC;aAAU8F;SAAQ;AACjDA,kBAAU,CAAA;MACZ;IACF,SAASzC,WAAW;AACpB,QAAIyC,QAAQ7D,SAAS,GAAG;AACtB,YAAM,KAAKxC,OAAOO,YAAY;QAAC;WAAU8F;OAAQ;IACnD;EACF;;;;;;;;;;;;;;;;;;;;;;;;;EA0BA,MAAME,YAAYvB,QAAkD;AAClE,UAAM1C,OAAOkE,OAAOlE,KAAK0C,MAAAA;AACzB,QAAI1C,KAAKE,WAAW,GAAG;AACrB,YAAM,IAAIiE,mBACR,gBACA,gMAEE;IAEN;AACA,UAAMC,YAAYpE,KAAK0C,OAAO,CAACnC,QAAQ,CAAC,KAAK/C,iBAAiB6G,SAAS9D,GAAAA,CAAAA;AACvE,QAAI6D,UAAUlE,SAAS,GAAG;AACxB,YAAMoE,QAAQF,UAAUxC,IAAI,CAACrB,QAAQ,IAAIA,GAAAA,GAAM,EAAEgE,KAAK,IAAA;AACtD,YAAMC,WAAW,KAAKhH,iBAAiB+G,KAAK,IAAA,KAAS;AACrD,YAAM,IAAIJ,mBACR,mBACA,iCAAiCG,KAAAA,oDAAyDE,QAAAA,0CAAkD;IAEhJ;AAGA,QAAI7B,qBAAqBD,MAAAA,GAAS;AAChC,aAAO;IACT;AAEA,UAAME,QAAQC,YAAYH,MAAAA;AAC1B,QAAI+B,UAAU;AACd,eAAS;AACP,YAAMlD,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;QAC1C;QACA,KAAKlB;QACL6F;QACA;QACA;QACA;QACA7D,OAAOiF,YAAAA;QACP;QACA;OACD;AACD,YAAMxC,OAAOkC,gBAAgBnC,KAAAA;AAC7B,UAAIC,KAAKtB,WAAW,GAAG;AACrB;MACF;AACA,YAAMwE,UAAUxD,OAAO,MAAM,KAAKxD,OAAOO,YAAY;QAAC;WAAUuD;OAAK,CAAA;AACrE,UAAI,CAACN,OAAOyD,SAASD,OAAAA,KAAYA,YAAY,GAAG;AAC9C;MACF;AACAD,iBAAWC;IACb;AACA,WAAOD;EACT;;;;;;EAOA,MAAMG,YAAYlC,QAAmD;AACnE,QAAIC,qBAAqBD,MAAAA,GAAS;AAChC,aAAO;IACT;AACA,UAAMnB,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;MAC1C;MACA,KAAKlB;MACL8F,YAAYH,MAAAA;MACZ;MACA;MACA;MACA;MACA;MACA;KACD;AACD,WAAOmC,iBAAiBtD,KAAAA;EAC1B;EAEA,MAAMuD,OAAOvF,WAAqB5B,SAAkD;AAClF,QAAIgF,qBAAqBhF,QAAQ+E,MAAM,GAAG;AACxC,aAAO,CAAA;IACT;AACA,UAAME,QAAQ,GAAGC,YAAYlF,QAAQ+E,MAAM,CAAA,UAAW/E,QAAQoH,IAAI;AAClE,UAAMxD,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;MAC1C;MACA,KAAKlB;MACL6F;MACA;MACA;MACA;MACAtD,aAAaC,SAAAA;MACb;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACAR,OAAOpB,QAAQoH,IAAI;KACpB;AACD,WAAO7B,iBAAiB3B,OAAO,KAAKjE,MAAM;EAC5C;;;;;;;;;;;;EAaA,MAAM0H,WAAWpC,OAAejF,SAAkD;AAChF,QAAIgF,qBAAqBhF,QAAQ+E,MAAM,GAAG;AACxC,aAAO,CAAA;IACT;AACA,UAAMuC,SAAQC,cAActC,KAAAA;AAC5B,QAAIqC,OAAM/E,WAAW,GAAG;AACtB,aAAO,CAAA;IACT;AACA,UAAMiF,cAActC,YAAYlF,QAAQ+E,MAAM;AAC9C,UAAM0C,YAAY,UAAUH,OAAMV,KAAK,GAAA,CAAA;AACvC,UAAMhD,QAAQ,MAAM,KAAK7D,OAAOO,YAAY;MAC1C;MACA,KAAKlB;MACLoI,gBAAgB,MAAMC,YAAY,IAAID,WAAAA,IAAeC,SAAAA;MACrD;MACA,KAAK3H;MACL;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACAsB,OAAOpB,QAAQoH,IAAI;KACpB;AACD,WAAO7B,iBAAiB3B,OAAO,KAAKjE,QAAQ,SAAA;EAC9C;AACF;AAGA,IAAMmG,YAAY;AAGlB,IAAMO,eAAe;AAGrB,SAASL,YAAYpD,KAAajD,QAAc;AAC9C,SAAOiD,IAAI8E,WAAW/H,MAAAA,IAAUiD,IAAIsB,MAAMvE,OAAO4C,MAAM,IAAIK;AAC7D;AAFSoD;AAST,SAASD,gBAAgBnC,OAAc;AACrC,MAAIgB,MAAMC,QAAQjB,KAAAA,GAAQ;AACxB,WAAOA,MAAMM,MAAM,CAAA,EAAGD,IAAIjB,KAAAA;EAC5B;AACA,MAAI,OAAOY,UAAU,YAAYA,UAAU,QAAQ,aAAaA,OAAO;AACrE,UAAM+D,UAAW/D,MAA+B+D;AAChD,WAAO/C,MAAMC,QAAQ8C,OAAAA,IAAWA,QAAQ1D,IAAI2D,MAAAA,IAAU,CAAA;EACxD;AACA,SAAO,CAAA;AACT;AATS7B;AAgBT,SAASmB,iBAAiBtD,OAAc;AACtC,MAAIgB,MAAMC,QAAQjB,KAAAA,GAAQ;AACxB,UAAMiE,QAAQtE,OAAOP,MAAMY,MAAM,CAAA,CAAE,CAAA;AACnC,WAAOL,OAAOyD,SAASa,KAAAA,IAASA,QAAQ;EAC1C;AACA,MAAI,OAAOjE,UAAU,YAAYA,UAAU,QAAQ,mBAAmBA,OAAO;AAC3E,UAAMiE,QAAQtE,OAAQK,MAAqCkE,aAAa;AACxE,WAAOvE,OAAOyD,SAASa,KAAAA,IAASA,QAAQ;EAC1C;AACA,SAAO;AACT;AAVSX;AA0BT,SAASxG,qBAAqBkD,OAAc;AAC1C,QAAMnD,aAAa,oBAAI2E,IAAAA;AACvB,aAAWjB,SAAS4D,cAAcnE,OAAO,YAAA,GAAe;AACtD,UAAMe,SAASqD,cAAc7D,KAAAA;AAC7B,UAAM1E,OAAOkF,OAAOsD,aAAatD,OAAOuD;AACxC,QAAIzI,SAASqB,QAAW;AACtB;IACF;AACA,UAAMqH,MAAM5E,OAAOoB,OAAOwD,GAAG;AAC7B1H,eAAWkF,IAAIlG,MAAM;MACnByB,OAAOyD,OAAOzD,QAAQ,IAAIkH,YAAW;MACrC,GAAIzD,OAAOwD,QAAQrH,UAAayC,OAAOyD,SAASmB,GAAAA,IAAO;QAAEvI,YAAYuI;MAAI,IAAI,CAAC;IAChF,CAAA;EACF;AACA,SAAO1H;AACT;AAfSC;AAkBT,SAASqH,cAAcnE,OAAgBhB,KAAW;AAChD,MAAIgC,MAAMC,QAAQjB,KAAAA,GAAQ;AACxB,aAASxE,QAAQ,GAAGA,QAAQ,IAAIwE,MAAMrB,QAAQnD,SAAS,GAAG;AACxD,UAAI4D,MAAMY,MAAMxE,KAAAA,CAAM,MAAMwD,KAAK;AAC/B,cAAMX,QAAQ2B,MAAMxE,QAAQ,CAAA;AAC5B,eAAOwF,MAAMC,QAAQ5C,KAAAA,IAASA,QAAQ,CAAA;MACxC;IACF;AACA,WAAO,CAAA;EACT;AACA,MAAI,OAAO2B,UAAU,YAAYA,UAAU,QAAQhB,OAAOgB,OAAO;AAC/D,UAAM3B,QAAS2B,MAAkChB,GAAAA;AACjD,WAAOgC,MAAMC,QAAQ5C,KAAAA,IAASA,QAAQ,CAAA;EACxC;AACA,SAAO,CAAA;AACT;AAfS8F;AAkBT,SAASC,cAAc7D,OAAc;AACnC,QAAMQ,SAA6C,CAAC;AACpD,MAAIC,MAAMC,QAAQV,KAAAA,GAAQ;AACxB,aAAS/E,QAAQ,GAAGA,QAAQ,IAAI+E,MAAM5B,QAAQnD,SAAS,GAAG;AACxDuF,aAAO3B,MAAMmB,MAAM/E,KAAAA,CAAM,EAAEiJ,YAAW,CAAA,IAAMrF,MAAMmB,MAAM/E,QAAQ,CAAA,CAAE;IACpE;AACA,WAAOuF;EACT;AACA,MAAI,OAAOR,UAAU,YAAYA,UAAU,MAAM;AAC/C,eAAW,CAACvB,KAAKX,KAAAA,KAAUsE,OAAO+B,QAAQnE,KAAAA,GAAQ;AAChDQ,aAAO/B,IAAIyF,YAAW,CAAA,IAAMrF,MAAMf,KAAAA;IACpC;EACF;AACA,SAAO0C;AACT;AAdSqD;AAiBT,SAASrG,aAAaC,WAAmB;AACvC,SAAO2G,OAAOC,KAAK,IAAIC,aAAa7G,SAAAA,EAAW8G,MAAM;AACvD;AAFS/G;AAUT,SAASuD,YAAYH,QAAgC;AACnD,MAAIA,WAAWjE,UAAayF,OAAOlE,KAAK0C,MAAAA,EAAQxC,WAAW,GAAG;AAC5D,WAAO;EACT;AACA,QAAMoG,UAAUpC,OAAO+B,QAAQvD,MAAAA,EAAQd,IAAI,CAAC,CAACrB,KAAKX,KAAAA,MAAM;AACtD,UAAM2D,SAAShB,MAAMC,QAAQ5C,KAAAA,IAASA,QAAQ;MAACA;;AAC/C,UAAM2G,cAAchD,OAAO3B,IAAI,CAACE,UAAU0E,UAAUzH,OAAO+C,KAAAA,CAAAA,CAAAA,EAASyC,KAAK,GAAA;AACzE,WAAO,SAAShE,GAAAA,KAAQgG,WAAAA;EAC1B,CAAA;AACA,SAAO,IAAID,QAAQ/B,KAAK,GAAA,CAAA;AAC1B;AAVS1B;AA0BT,SAASqC,cAActC,OAAa;AAClC,SAAOA,MAAM6D,MAAM,kBAAA,KAAuB,CAAA;AAC5C;AAFSvB;AAaT,SAASrF,UAAUD,OAAc;AAC/B,SAAO2C,MAAMC,QAAQ5C,KAAAA,IAASA,MAAMgC,IAAI7C,MAAAA,EAAQwF,KAAK,GAAA,IAAOxF,OAAOa,KAAAA;AACrE;AAFSC;AAKT,SAAS2G,UAAU5G,OAAa;AAC9B,SAAOA,MAAM8G,QAAQ,kBAAkB,CAACC,SAAS,KAAKA,IAAAA,EAAM;AAC9D;AAFSH;AAKT,SAASnF,WAAWzB,OAAa;AAC/B,SAAOA,MAAM8G,QAAQ,cAAc,CAACC,SAAS,KAAKA,IAAAA,EAAM;AAC1D;AAFStF;AAQT,SAASI,eAAeF,OAAc;AACpC,MAAIgB,MAAMC,QAAQjB,KAAAA,KAAUA,MAAMrB,UAAU,GAAG;AAC7C,UAAM0G,UAAUrF,MAAM,CAAA;AACtB,WAAO;MACLD,QAAQX,MAAMY,MAAM,CAAA,CAAE;MACtBvB,MAAMuC,MAAMC,QAAQoE,OAAAA,IAAWA,QAAQhF,IAAIjB,KAAAA,IAAS,CAAA;IACtD;EACF;AACA,MAAI,OAAOY,UAAU,YAAYA,UAAU,QAAQ,YAAYA,SAAS,UAAUA,OAAO;AACvF,UAAM,EAAED,QAAQtB,KAAI,IAAKuB;AACzB,WAAO;MACLD,QAAQX,MAAMW,MAAAA;MACdtB,MAAMuC,MAAMC,QAAQxC,IAAAA,IAAQA,KAAK4B,IAAIjB,KAAAA,IAAS,CAAA;IAChD;EACF;AACA,SAAO;IAAEW,QAAQ;IAAKtB,MAAM,CAAA;EAAG;AACjC;AAhBSyB;AAkBT,SAASd,MAAMf,OAAc;AAC3B,MAAI,OAAOA,UAAU,UAAU;AAC7B,WAAOA;EACT;AACA,SAAOsG,OAAOW,SAASjH,KAAAA,IAASA,MAAMkH,SAAS,MAAA,IAAU/H,OAAOa,KAAAA;AAClE;AALSe;AAwBT,SAASuC,iBACP3B,OACAjE,QACAyJ,cAA2B,UAAQ;AAEnC,QAAMC,SAASD,gBAAgB,YAAY,IAAI;AAC/C,MAAIxE,MAAMC,QAAQjB,KAAAA,GAAQ;AACxB,UAAM0B,WAAsB,CAAA;AAC5B,aAASlG,QAAQ,GAAGA,QAAQiK,SAAS,IAAIzF,MAAMrB,QAAQnD,SAASiK,QAAQ;AACtE,YAAMC,YAAY1F,MAAMxE,QAAQiK,SAAS,CAAA;AACzC,UAAIzE,MAAMC,QAAQyE,SAAAA,GAAY;AAC5B,cAAMC,QAAgC,CAAC;AACvC,iBAASlK,QAAQ,GAAGA,QAAQ,IAAIiK,UAAU/G,QAAQlD,SAAS,GAAG;AAC5DkK,gBAAMvG,MAAMsG,UAAUjK,KAAAA,CAAM,CAAA,IAAK2D,MAAMsG,UAAUjK,QAAQ,CAAA,CAAE;QAC7D;AACA,cAAMmK,cAAcJ,gBAAgB,YAAYpG,MAAMY,MAAMxE,QAAQ,CAAA,CAAE,IAAI0B;AAC1EwE,iBAASrE,KAAKwI,WAAUzG,MAAMY,MAAMxE,KAAAA,CAAM,GAAGmK,OAAO5J,QAAQyJ,aAAaI,WAAAA,CAAAA;MAC3E;IACF;AACA,WAAOlE;EACT;AACA,MAAI,OAAO1B,UAAU,YAAYA,UAAU,QAAQ,aAAaA,OAAO;AACrE,UAAM+D,UAAW/D,MAA+B+D;AAChD,QAAI/C,MAAMC,QAAQ8C,OAAAA,GAAU;AAC1B,aAAOA,QAAQ1D,IAAI,CAACyF,WAClBD,WACE7B,OAAO8B,MAAAA,GACPC,eAAeD,MAAAA,GACf/J,QACAyJ,aACAA,gBAAgB,YAAYQ,UAAUF,MAAAA,IAAU5I,MAAAA,CAAAA;IAGtD;EACF;AACA,SAAO,CAAA;AACT;AApCSyE;AAuCT,SAASqE,UAAUF,QAAe;AAChC,MAAI,OAAOA,WAAW,YAAYA,WAAW,QAAQ,WAAWA,QAAQ;AACtE,WAAO1G,MAAO0G,OAA8BG,KAAK;EACnD;AACA,SAAO/I;AACT;AALS8I;AAOT,SAAShC,OAAO8B,QAAe;AAC7B,MAAI,OAAOA,WAAW,YAAYA,WAAW,QAAQ,QAAQA,QAAQ;AACnE,WAAO1G,MAAO0G,OAA2BjI,EAAE;EAC7C;AACA,SAAO;AACT;AALSmG;AAOT,SAAS+B,eAAeD,QAAe;AACrC,QAAMH,QAAgC,CAAC;AACvC,MAAI,OAAOG,WAAW,YAAYA,WAAW,QAAQ,sBAAsBA,QAAQ;AACjF,UAAM7G,MAAO6G,OAAyCI;AACtD,QAAI,OAAOjH,QAAQ,YAAYA,QAAQ,MAAM;AAC3C,iBAAW,CAACD,KAAKX,KAAAA,KAAUsE,OAAO+B,QAAQzF,GAAAA,GAAM;AAC9C0G,cAAM3G,GAAAA,IAAOI,MAAMf,KAAAA;MACrB;IACF;EACF;AACA,SAAOsH;AACT;AAXSI;AAaT,SAASF,WACPM,OACAR,OACA5J,QACAyJ,cAA2B,UAC3BI,aAAoB;AAEpB,QAAMK,QACJT,gBAAgB,YAAY7F,OAAOiG,eAAe,GAAA,IAAO,IAAIjG,OAAOgG,MAAMS,gBAAgB,GAAA;AAC5F,QAAMnI,SAAS0H,MAAM1H;AACrB,SAAO;IACLJ,IAAIsI,MAAMrC,WAAW/H,MAAAA,IAAUoK,MAAM7F,MAAMvE,OAAO4C,MAAM,IAAIwH;IAC5DrI,MAAM6H,MAAM7H,QAAQ;IACpBmI,OAAOtG,OAAOyD,SAAS6C,KAAAA,IAASA,QAAQ;IACxC,GAAIhI,WAAWf,SAAY;MAAEe;IAAO,IAAI,CAAC;IACzC,GAAGkB,cAAcwG,MAAMU,aAAa;EACtC;AACF;AAjBSR,OAAAA,YAAAA;AAmBT,SAAS1G,cAAcF,KAAuB;AAC5C,MAAIA,QAAQ/B,QAAW;AACrB,WAAO,CAAC;EACV;AACA,MAAI;AACF,UAAMoJ,SAAkBnI,KAAKoI,MAAMtH,GAAAA;AACnC,QAAI,OAAOqH,WAAW,YAAYA,WAAW,MAAM;AACjD,aAAO;QAAEpI,UAAUoI;MAAkC;IACvD;EACF,QAAQ;EAER;AACA,SAAO,CAAC;AACV;AAbSnH;;;AC18BF,SAASqH,mBAAmBC,OAAc;AAC/C,SACE,OAAOA,UAAU,YACjBA,UAAU,QACV,OAAOC,QAAQC,IAAIF,OAAO,mBAAA,MAAyB;AAEvD;AANgBD;AAST,SAASI,kBAAkBC,YAAkB;AAClD,SAAOL,mBAAmBK,UAAAA,IAAaA,WAAUD,kBAAiB,IAAK,CAAC;AAC1E;AAFgBA;AAaT,SAASE,eAAeC,MAAa;AAC1C,QAAM,EAAEC,WAAWC,UAAU,GAAGC,OAAAA,IAAWN,kBAAkBG,IAAAA;AAC7D,SAAOG;AACT;AAHgBJ;AAcT,SAASK,qBACdC,MAAwB;AAExB,QAAMC,UAAUD,KAAKE,IAAIR,cAAAA;AACzB,QAAM,CAACS,KAAAA,IAASF;AAChB,MAAIE,UAAUC,QAAW;AACvB,WAAO,CAAC;EACV;AACA,QAAMC,SAASJ,QAAQK,MACrB,CAACR,WAAWA,OAAOS,UAAUJ,MAAMI,SAAST,OAAOU,eAAeL,MAAMK,UAAU;AAEpF,SAAOH,SAASF,QAAQ,CAAC;AAC3B;AAZgBJ;;;AClDT,IAAMU,qBAAN,MAAMA;EAhCb,OAgCaA;;;;;;EACX,YACmBC,UACAC,OACAC,UAAqC,CAAC,GACvD;SAHiBF,WAAAA;SACAC,QAAAA;SACAC,UAAAA;EAChB;;EAGHC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;MAAa,GAAGC,eAAe,KAAKJ,KAAK;IAAE;EACjE;EAEA,MAAMK,SAASC,OAAeL,UAA2B,CAAC,GAAuB;AAC/E,UAAM,CAACM,SAAAA,IAAa,MAAM,KAAKR,SAASS,MAAM;MAACF;KAAM;AACrD,QAAIC,cAAcE,QAAW;AAC3B,aAAO,CAAA;IACT;AACA,UAAMC,WAAW,MAAM,KAAKV,MAAMW,OAAOJ,WAAW;MAClDK,MAAMX,QAAQW,QAAQ;MACtB,GAAIX,QAAQY,WAAWJ,SAAY;QAAEI,QAAQZ,QAAQY;MAAO,IAAI,CAAC;IACnE,CAAA;AACA,UAAM,EAAEC,SAAQ,IAAK,KAAKb;AAK1B,WAAOa,aAAaL,SAChBC,WACAA,SAASG,OAAO,CAACE,YAAYA,QAAQC,SAASF,QAAAA;EACpD;AACF;;;ACrCO,IAAMG,mBAAN,MAAMA;EA9Bb,OA8BaA;;;EACMC,OAAO,oBAAIC,IAAAA;EACXC,oBAAoB,oBAAID,IAAAA;EACjCE,cAAc;EACLC;EACAC;EAEjB,YAAYC,UAAmC,CAAC,GAAG;AACjD,SAAKF,KAAKE,QAAQF,MAAM;AACxB,SAAKC,IAAIC,QAAQD,KAAK;EACxB;;;;;;EAOAE,oBAAyC;AACvC,WAAO;MAAEC,WAAW;IAAU;EAChC;;EAGA,IAAIC,OAAe;AACjB,WAAO,KAAKT,KAAKS;EACnB;;EAGAC,IAAIC,SAA8B;AAChC,eAAWC,UAAUD,SAAS;AAC5B,WAAKE,YAAYD,OAAOE,EAAE;AAC1B,YAAMC,SAASC,SAASJ,OAAOK,IAAI;AACnC,YAAMC,cAAc,oBAAIjB,IAAAA;AACxB,iBAAWkB,SAASJ,QAAQ;AAC1BG,oBAAYE,IAAID,QAAQD,YAAYG,IAAIF,KAAAA,KAAU,KAAK,CAAA;MACzD;AACA,iBAAWG,QAAQJ,YAAYK,KAAI,GAAI;AACrC,aAAKrB,kBAAkBkB,IAAIE,OAAO,KAAKpB,kBAAkBmB,IAAIC,IAAAA,KAAS,KAAK,CAAA;MAC7E;AACA,WAAKtB,KAAKoB,IAAIR,OAAOE,IAAI;QAAEF;QAAQM;QAAaM,QAAQT,OAAOS;MAAO,CAAA;AACtE,WAAKrB,eAAeY,OAAOS;IAC7B;EACF;;;;;;;;EASAC,OAAOC,YAA0B;AAC/B,eAAWZ,MAAM;SAAI,KAAKd,KAAKuB,KAAI;OAAK;AACtC,UAAIT,OAAOY,cAAcC,aAAab,EAAAA,MAAQY,YAAY;AACxD,aAAKb,YAAYC,EAAAA;MACnB;IACF;EACF;;EAGAc,QAAc;AACZ,SAAK5B,KAAK4B,MAAK;AACf,SAAK1B,kBAAkB0B,MAAK;AAC5B,SAAKzB,cAAc;EACrB;;EAGQU,YAAYC,IAAkB;AACpC,UAAMe,WAAW,KAAK7B,KAAKqB,IAAIP,EAAAA;AAC/B,QAAIe,aAAaC,QAAW;AAC1B;IACF;AACA,eAAWR,QAAQO,SAASX,YAAYK,KAAI,GAAI;AAC9C,YAAMQ,QAAQ,KAAK7B,kBAAkBmB,IAAIC,IAAAA,KAAS,KAAK;AACvD,UAAIS,QAAQ,GAAG;AACb,aAAK7B,kBAAkB8B,OAAOV,IAAAA;MAChC,OAAO;AACL,aAAKpB,kBAAkBkB,IAAIE,MAAMS,IAAAA;MACnC;IACF;AACA,SAAK5B,eAAe0B,SAASL;AAC7B,SAAKxB,KAAKgC,OAAOlB,EAAAA;EACnB;EAEA,MAAMmB,SAASC,OAAe5B,UAA2B,CAAC,GAAuB;AAC/E,UAAM6B,QAAQ,KAAKnC,KAAKS;AACxB,QAAI0B,UAAU,GAAG;AACf,aAAO,CAAA;IACT;AACA,UAAMC,gBAAgB,KAAKjC,cAAcgC;AACzC,UAAME,aAAa,IAAIC,IAAItB,SAASkB,KAAAA,CAAAA;AACpC,UAAMK,SAAoB,CAAA;AAC1B,eAAW,EAAE3B,QAAQM,aAAaM,OAAM,KAAM,KAAKxB,KAAKwC,OAAM,GAAI;AAChE,UAAIlC,QAAQmC,WAAWX,UAAa,CAACY,cAAc9B,OAAO+B,UAAUrC,QAAQmC,MAAM,GAAG;AACnF;MACF;AACA,UAAIG,QAAQ;AACZ,iBAAWtB,QAAQe,YAAY;AAC7B,cAAMQ,gBAAgB3B,YAAYG,IAAIC,IAAAA;AACtC,YAAIuB,kBAAkBf,QAAW;AAC/B;QACF;AACA,cAAMgB,KAAK,KAAK5C,kBAAkBmB,IAAIC,IAAAA,KAAS;AAC/C,cAAMyB,MAAMC,KAAKC,IAAI,KAAKd,QAAQW,KAAK,QAAQA,KAAK,IAAE;AACtD,cAAMI,cACJL,gBAAgB,KAAKzC,MAAM,IAAI,KAAKC,IAAK,KAAKA,IAAImB,SAAUY;AAC9DQ,iBAASG,OAAQF,iBAAiB,KAAKzC,KAAK,KAAM8C;MACpD;AACA,UAAIN,QAAQ,GAAG;AACbL,eAAOY,KAAK;UACVrC,IAAIF,OAAOE;UACXG,MAAML,OAAOK;UACb2B;UACA,GAAIhC,OAAOwC,WAAWtB,SAAY;YAAEsB,QAAQxC,OAAOwC;UAAO,IAAI,CAAC;UAC/D,GAAIxC,OAAO+B,aAAab,SAAY;YAAEa,UAAU/B,OAAO+B;UAAS,IAAI,CAAC;QACvE,CAAA;MACF;IACF;AACA,WAAOJ,OAAOc,KAAK,CAACC,GAAGjD,MAAMA,EAAEuC,QAAQU,EAAEV,KAAK,EAAEW,MAAM,GAAGjD,QAAQkD,QAAQ,CAAA;EAC3E;AACF;AAEA,SAASxC,SAASC,MAAY;AAC5B,SAAOA,KAAKwC,YAAW,EAAGC,MAAM,YAAA,KAAiB,CAAA;AACnD;AAFS1C;;;AC7HF,IAAM2C,mBAAN,MAAMA;EA1Bb,OA0BaA;;;;EACX,YAA6BC,OAA2B;SAA3BA,QAAAA;EAA4B;;EAGzDC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;MAAW,GAAGC,eAAe,KAAKH,KAAK;IAAE;EAC/D;EAEA,MAAMI,SAASC,OAAeC,UAA2B,CAAC,GAAuB;AAC/E,WAAO,KAAKN,MAAMO,WAAWF,OAAO;MAClCG,MAAMF,QAAQE,QAAQ;MACtB,GAAIF,QAAQG,WAAWC,SAAY;QAAED,QAAQH,QAAQG;MAAO,IAAI,CAAC;IACnE,CAAA;EACF;AACF;;;ACOO,IAAME,kBAAN,MAAMA;EA/Cb,OA+CaA;;;;;EACX,YACmBC,YACAC,UAAkC,CAAC,GACpD;SAFiBD,aAAAA;SACAC,UAAAA;EAChB;;EAGHC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;MAAU,GAAGC,qBAAqB,KAAKJ,UAAU;IAAE;EACzE;EAEA,MAAMK,SAASC,OAAeL,UAA2B,CAAC,GAAuB;AAC/E,UAAMM,IAAI,KAAKN,QAAQM,KAAK;AAC5B,UAAMC,YAAY,KAAKP,QAAQO,aAAa;AAC5C,UAAMC,UAAU,KAAKR,QAAQQ;AAC7B,UAAMC,QAAQ,MAAMC,QAAQC,IAC1B,KAAKZ,WAAWa,IAAI,CAACV,cACnBA,UAAUE,SAASC,OAAO;MACxBQ,MAAMN;MACN,GAAIP,QAAQc,WAAWC,SAAY;QAAED,QAAQd,QAAQc;MAAO,IAAI,CAAC;IACnE,CAAA,CAAA,CAAA;AAIJ,UAAME,QAAQ,oBAAIC,IAAAA;AAClBR,UAAMS,QAAQ,CAACC,MAAMC,cAAAA;AACnB,YAAMC,SAASb,UAAUY,SAAAA,KAAc;AACvCD,WAAKD,QAAQ,CAACI,SAASC,SAAAA;AACrB,cAAMC,eAAeH,UAAUf,IAAIiB,OAAO;AAC1C,cAAME,WAAWT,MAAMU,IAAIJ,QAAQK,EAAE;AACrC,YAAIF,aAAaV,QAAW;AAC1BU,mBAASG,SAASJ;QACpB,OAAO;AACLR,gBAAMa,IAAIP,QAAQK,IAAI;YAAEL;YAASM,OAAOJ;UAAa,CAAA;QACvD;MACF,CAAA;IACF,CAAA;AAEA,WAAO;SAAIR,MAAMc,OAAM;MACpBC,KAAK,CAACC,GAAGC,MAAMA,EAAEL,QAAQI,EAAEJ,KAAK,EAChCM,MAAM,GAAGlC,QAAQa,QAAQ,CAAA,EACzBD,IAAI,CAACuB,WAAW;MAAE,GAAGA,MAAMb;MAASM,OAAOO,MAAMP;IAAM,EAAA;EAC5D;AACF;;;AC7EO,IAAMQ,qBAAN,MAAMA;EAbb,OAaaA;;;;;;EACX,YACmBC,MACAC,UACAC,UAAqC,CAAC,GACvD;SAHiBF,OAAAA;SACAC,WAAAA;SACAC,UAAAA;EAChB;;EAGHC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;MAAa,GAAGC,eAAe,KAAKL,IAAI;IAAE;EAChE;EAEA,MAAMM,SAASC,OAAeL,UAA2B,CAAC,GAAuB;AAC/E,UAAMM,aAAa,MAAM,KAAKR,KAAKM,SAASC,OAAO;MACjDE,MAAM,KAAKP,QAAQQ,aAAa;MAChC,GAAIR,QAAQS,WAAWC,SAAY;QAAED,QAAQT,QAAQS;MAAO,IAAI,CAAC;IACnE,CAAA;AACA,WAAO,KAAKV,SAASY,OAAON,OAAOC,YAAY;MAAEC,MAAMP,QAAQO,QAAQ;IAAE,CAAA;EAC3E;AACF;;;ACrBO,IAAMK,oBAAN,MAAMA;EAXb,OAWaA;;;;;EACX,YACmBC,MACAC,QACjB;SAFiBD,OAAAA;SACAC,SAAAA;EAChB;;EAGHC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;MAAY,GAAGC,eAAe,KAAKJ,IAAI;IAAE;EAC/D;EAEA,MAAMK,SAASC,OAAeC,UAA2B,CAAC,GAAuB;AAC/E,WAAO,KAAKP,KAAKK,SAASC,OAAO;MAC/B,GAAGC;MACHN,QAAQ;QAAE,GAAGM,QAAQN;QAAQ,GAAG,KAAKA;MAAO;IAC9C,CAAA;EACF;AACF;;;ACGO,SAASO,eACdC,WACAC,UAAwB,CAAC,GAAC;AAE1B,QAAMC,SAAwB,CAAA;AAC9B,aAAWC,YAAYH,WAAW;AAChCI,cAAUD,SAASE,MAAMJ,OAAAA,EAASK,QAAQ,CAACD,MAAME,UAAAA;AAC/CL,aAAOM,KAAK;QACVC,IAAI,GAAGN,SAASM,EAAE,IAAIF,KAAAA;QACtBF;QACA,GAAIF,SAASO,WAAWC,SAAY;UAAED,QAAQP,SAASO;QAAO,IAAI,CAAC;QACnE,GAAIP,SAASS,aAAaD,SAAY;UAAEC,UAAUT,SAASS;QAAS,IAAI,CAAC;MAC3E,CAAA;IACF,CAAA;EACF;AACA,SAAOV;AACT;AAhBgBH;AAmBhB,eAAsBc,aACpBX,QACAD,SAA4B;AAE5B,MAAIC,OAAOY,WAAW,GAAG;AACvB,WAAO;EACT;AACA,QAAMC,aAAa,MAAMd,QAAQe,SAASC,MAAMf,OAAOgB,IAAI,CAACC,UAAUA,MAAMd,IAAI,CAAA;AAChF,QAAMe,UAA0BlB,OAAOgB,IAAI,CAACC,OAAOZ,WAAW;IAC5D,GAAGY;IACHE,WAAWN,WAAWR,KAAAA,KAAU,CAAA;EAClC,EAAA;AACA,QAAMN,QAAQqB,MAAMC,OAAOH,OAAAA;AAC3B,SAAOA,QAAQN;AACjB;AAdsBD;AAkBtB,eAAsBW,gBACpBxB,WACAC,SAAsB;AAEtB,SAAOY,aAAad,eAAeC,WAAWC,OAAAA,GAAUA,OAAAA;AAC1D;AALsBuB;;;ACrEtB,8BAAkC;AAElC,gCAA8C;AA+DvC,IAAMC,qBAAqB;AAG3B,IAAMC,sBAAsB;AAS5B,IAAMC,4BAAwBC,uCAAYH,oBAAoBC,mBAAAA;AA2C9D,SAASG,iBAAiBC,OAA0BC,YAAkB;AAC3EC,sCAAKP,oBAAoBC,qBAAqBI,OAAO;IAAEC;EAAW,CAAA;AACpE;AAFgBF;AAmChB,IAAMI,gBAAgB;AAMtB,IAAMC,cAAc,IAAIC,0CAAAA;AAGxB,SAASC,OAAOC,OAAa;AAC3B,SAAOC,KAAKC,MAAMF,QAAQ,GAAA,IAAU;AACtC;AAFSD;AAKT,SAASI,WAAWC,UAAmB;AACrC,QAAMC,SAASD,SAASE,IAAI,CAACC,YAAYA,QAAQC,KAAK,EAAEC,OAAO,CAACD,UAAUE,OAAOC,SAASH,KAAAA,CAAAA;AAC1F,MAAIH,OAAOO,WAAW,GAAG;AACvB,WAAO,CAAC;EACV;AACA,QAAMC,MAAMR,OAAOS,OAAO,CAACC,OAAOP,UAAUO,QAAQP,OAAO,CAAA;AAC3D,SAAO;IAAEQ,UAAUjB,OAAOE,KAAKgB,IAAG,GAAIZ,MAAAA,CAAAA;IAAUa,WAAWnB,OAAOc,MAAMR,OAAOO,MAAM;EAAE;AACzF;AAPST;AAST,IAAMgB,wBAAN,MAAMA,uBAAAA;EAlLN,OAkLMA;;;;;EACJ,YACmBC,MACAC,UAAsC,CAAC,GACxD;SAFiBD,OAAAA;SACAC,UAAAA;EAChB;;EAGHC,oBAAyC;AACvC,WAAO;MAAE,GAAGA,kBAAkB,KAAKF,IAAI;MAAG,GAAG,KAAKC,QAAQE;IAAS;EACrE;EAEA,MAAMC,SAASC,OAAeJ,UAA2B,CAAC,GAAuB;AAG/E,QAAI,KAACK,sCAAWtC,oBAAoBC,mBAAAA,EAAqBsC,gBAAgB;AACvE,aAAO,KAAKP,KAAKI,SAASC,OAAOJ,OAAAA;IACnC;AACA,QAAIxB,YAAY+B,SAAQ,MAAO,MAAM;AACnC,aAAO,KAAKR,KAAKI,SAASC,OAAOJ,OAAAA;IACnC;AAEA,UAAMQ,aAAa,KAAKP,kBAAiB;AACzC,UAAMQ,OAAOT,QAAQS,QAAQlC;AAC7B,UAAMmC,YAAYC,KAAKC,IAAG;AAC1B,QAAI;AACF,YAAM7B,WAAW,MAAMP,YAAYqC,IAAI,MAAM,MAAM,KAAKd,KAAKI,SAASC,OAAOJ,OAAAA,CAAAA;AAC7E7B,uBACE;QACE2C,WAAWN,WAAWM,aAAa;QACnC,GAAIN,WAAWO,UAAUC,SAAY;UAAED,OAAOP,WAAWO;QAAM,IAAI,CAAC;QACpE,GAAG,KAAKE,aAAajB,QAAQZ,QAAQoB,UAAAA;QACrCC;QACAS,QAAQnC,SAASQ;QACjB4B,SAASpC,SAASQ,WAAW;QAC7B,GAAGT,WAAWC,QAAAA;QACdqC,QAAQ;MACV,GACAT,KAAKC,IAAG,IAAKF,SAAAA;AAEf,aAAO3B;IACT,SAASsC,OAAO;AACdlD,uBACE;QACE2C,WAAWN,WAAWM,aAAa;QACnC,GAAIN,WAAWO,UAAUC,SAAY;UAAED,OAAOP,WAAWO;QAAM,IAAI,CAAC;QACpE,GAAG,KAAKE,aAAajB,QAAQZ,QAAQoB,UAAAA;QACrCC;QACAS,QAAQ;QACRC,SAAS;QACTC,QAAQ;QACRC,OAAOA,iBAAiBC,QAAQD,MAAME,UAAUC,OAAOH,KAAAA;MACzD,GACAV,KAAKC,IAAG,IAAKF,SAAAA;AAEf,YAAMW;IACR;EACF;;EAGQJ,aACN7B,QACAoB,YACyB;AACzB,UAAM,EAAEiB,WAAU,IAAK,KAAKzB;AAC5B,UAAM0B,WACJ,OAAOD,eAAe,aAAaA,WAAWrC,MAAAA,IAAWqC,cAAcjB,WAAWiB;AACpF,WAAOC,aAAaV,SAAY;MAAES,YAAYC;IAAS,IAAI,CAAC;EAC9D;AACF;AAkBO,SAASC,oBACdb,WACAd,UAAsC,CAAC,GAAC;AAExC,SAAO,IAAIF,sBAAsBgB,WAAWd,OAAAA;AAC9C;AALgB2B;;;AC1OhB,SAASC,aAAaC,OAAc;AAClC,SACE,OAAOA,UAAU,YACjBA,UAAU,QACV,WAAWA,SACX,OAAQA,MAAmCC,UAAU;AAEzD;AAPSF;AAUT,IAAMG,cAAmD;EACvD,aAAa;IACXC,SAAS;IACTC,QAAQ;IACRC,SAASL,OAAc;AACrB,aAAOD,aAAaC,KAAAA,IAChB;QAAEA,OAAO;UAAEC,OAAOD,MAAMC;QAAM;MAAE,IAChC;QAAEK,QAAQ;UAAC;YAAEC,SAAS;UAA2C;;MAAG;IAC1E;EACF;AACF;AAeO,SAASC,oBACdC,WACAC,UAAgC,CAAC,GAAC;AAElC,QAAMC,OAAOD,QAAQC;AACrB,QAAM,EAAEC,YAAY,KAAI,IAAKF;AAC7B,QAAMG,eACJD,cAAc,QACVH,YACAK,oBAAoBL,WAAWG,cAAc,OAAO,CAAC,IAAIA,SAAAA;AAC/D,SAAO;IACLG,MAAM;MACJC,MAAMN,QAAQM,QAAQ;MACtBC,MAAM;MACNC,aACER,QAAQQ,eACR;MACFC,aAAajB;IACf;IACAkB,SAAS;MACP,MAAMC,QAAQC,OAAc;AAC1B,YAAI,CAACvB,aAAauB,KAAAA,GAAQ;AACxB,gBAAM,IAAIC,MAAM,0CAAA;QAClB;AACA,cAAMC,WAAW,MAAMX,aAAaY,SAClCH,MAAMrB,OACNU,SAASe,SAAY;UAAEf;QAAK,IAAI,CAAC,CAAA;AAEnC,eAAO;UAAEa;QAAS;MACpB;IACF;EACF;AACF;AAhCgBhB;;;AC9DT,IAAMmB,iBAAN,cAA6BC,MAAAA;EADpC,OACoCA;;;;EAClC,YAEWC,QACTC,SACA;AACA,UAAMA,OAAAA,GAAAA,KAHGD,SAAAA;AAIT,SAAKE,OAAO;EACd;AACF;AAiDO,SAASC,gBAAgBC,OAAc;AAC5C,MAAI,EAAEA,iBAAiBN,iBAAiB;AACtC,WAAO;EACT;AACA,MAAIM,MAAMJ,WAAW,KAAK;AACxB,WAAO;EACT;AACA,UACGI,MAAMJ,WAAW,OAAOI,MAAMJ,WAAW,QAC1C,2FAA2FK,KACzFD,MAAMH,OAAO;AAGnB;AAbgBE;AAoBhB,IAAMG,qBAAqB,oBAAIC,IAAAA;AAiBxB,SAASC,iBAAiBC,SAAgC;AAC/D,QAAMC,MAAM,IAAID,QAAQE,WAAW,6BAA6BC,QAAQ,QAAQ,EAAA,CAAA;AAChF,QAAMC,WAAW,GAAGH,GAAAA,IAAOD,QAAQK,KAAK;AACxC,QAAMC,aAAaC,KAAKC,IAAI,GAAGD,KAAKE,MAAMT,QAAQU,aAAa,EAAA,CAAA;AAC/D,MAAIA,YAAYH,KAAKI,IAAIL,YAAYT,mBAAmBe,IAAIR,QAAAA,KAAaE,UAAAA;AACzE,QAAMO,gBAAgBb,QAAQa,iBAAiB;AAC/C,QAAMC,YAAYd,QAAQc,aAAa;AACvC,QAAMC,UAAUf,QAAQgB,SAASA;AAEjC,QAAMC,UAAU,8BAAOC,UAAAA;AACrB,UAAMC,WAAW,MAAMJ,QAAQd,KAAK;MAClCmB,QAAQ;MACRC,SAAS;QACP,gBAAgB;QAChB,GAAIrB,QAAQsB,SAAS;UAAEC,eAAe,UAAUvB,QAAQsB,MAAM;QAAG,IAAI,CAAC;QACtE,GAAGtB,QAAQqB;MACb;MACAG,MAAMC,KAAKC,UAAU;QACnBrB,OAAOL,QAAQK;QACfa;QACA,GAAIlB,QAAQ2B,eAAeC,SAAY;UAAED,YAAY3B,QAAQ2B;QAAW,IAAI,CAAC;MAC/E,CAAA;MACAE,QAAQC,YAAYC,QAAQjB,SAAAA;IAC9B,CAAA;AACA,UAAMU,OAAO,MAAML,SAASa,KAAI;AAChC,QAAI,CAACb,SAASc,IAAI;AAChB,YAAM,IAAI5C,eACR8B,SAAS5B,QACT,8BAA8B4B,SAAS5B,MAAM,MAAM2C,eAAeV,IAAAA,CAAAA,EAAO;IAE7E;AACA,UAAMW,QAAOC,UAAUZ,IAAAA;AAIvB,UAAMa,QAAQ;SAAKF,OAAMG,QAAQ,CAAA;MAAKC,KAAK,CAACC,GAAGC,OAAOD,EAAEE,SAAS,MAAMD,EAAEC,SAAS,EAAA;AAClF,QAAIL,MAAMM,WAAWzB,MAAMyB,UAAU,CAACN,MAAMO,MAAM,CAACC,SAASC,MAAMC,QAAQF,KAAKG,SAAS,CAAA,GAAI;AAC1F,YAAM,IAAI3D,eACR,KACA,uBAAuBgD,MAAMM,MAAM,gBAAgBzB,MAAMyB,MAAM,SAAS;IAE5E;AACA,UAAMM,SAASd,OAAMe,OAAOC,gBAAgBhB,OAAMe,OAAOE;AACzD,QAAI,OAAOH,WAAW,YAAYI,OAAOC,SAASL,MAAAA,GAAS;AACzDjD,cAAQuD,UAAUN,MAAAA;IACpB;AACA,WAAOZ,MAAMmB,IAAI,CAACX,SAASA,KAAKG,SAAS;EAC3C,GAtCgB;AA0ChB,QAAMS,mBAAmB,8BAAOvC,UAAAA;AAC9B,QAAIA,MAAMyB,SAASjC,WAAW;AAE5B,YAAMgD,UAAsB,CAAA;AAC5B,eAASC,QAAQ,GAAGA,QAAQzC,MAAMyB,QAAQgB,SAASjD,WAAW;AAC5DgD,gBAAQE,KAAI,GAAK,MAAMH,iBAAiBvC,MAAM2C,MAAMF,OAAOA,QAAQjD,SAAAA,CAAAA,CAAAA;MACrE;AACA,aAAOgD;IACT;AACA,QAAI;AACF,aAAO,MAAMzC,QAAQC,KAAAA;IACvB,SAASvB,OAAO;AACd,UAAI,CAACD,gBAAgBC,KAAAA,KAAUuB,MAAMyB,UAAU,GAAG;AAChD,cAAMhD;MACR;AACA,YAAMmE,OAAOvD,KAAKwD,KAAK7C,MAAMyB,SAAS,CAAA;AACtC,UAAImB,OAAOpD,WAAW;AACpBA,oBAAYoD;AACZjE,2BAAmBmE,IAAI5D,UAAU0D,IAAAA;AACjC9D,gBAAQiE,SACN,qBAAqBhE,GAAAA,uBAA0BiB,MAAMyB,MAAM,YAAahD,MAAgBH,OAAO,6BAA6BsE,IAAAA,qFAAyF;MAEzN;AACA,aAAO;WACD,MAAML,iBAAiBvC,MAAM2C,MAAM,GAAGC,IAAAA,CAAAA;WACtC,MAAML,iBAAiBvC,MAAM2C,MAAMC,IAAAA,CAAAA;;IAE3C;EACF,GA5ByB;AA8BzB,SAAO;IACL,MAAMI,MAAMC,OAAe;AACzB,YAAMT,UAAsB,CAAA;AAC5B,eAASC,QAAQ,GAAGA,QAAQQ,MAAMxB,UAAU;AAC1C,cAAMzB,QAAQiD,MACXN,MAAMF,OAAOA,QAAQjD,SAAAA,EACrB8C,IACC,CAACxB,UAAUA,KAAKW,SAAS9B,gBAAgBmB,KAAK6B,MAAM,GAAGhD,aAAAA,IAAiBmB,SAAS,GAAA;AAErF0B,gBAAQE,KAAI,GAAK,MAAMH,iBAAiBvC,KAAAA,CAAAA;AACxCyC,iBAASzC,MAAMyB;MACjB;AACA,aAAOe;IACT;EACF;AACF;AAhGgB3D;AAkGhB,SAASqC,UAAUZ,MAAY;AAC7B,MAAI;AACF,WAAOC,KAAK2C,MAAM5C,IAAAA;EACpB,QAAQ;AACN,WAAO;EACT;AACF;AANSY;AASF,SAASF,eAAeV,MAAY;AACzC,QAAMW,QAAOC,UAAUZ,IAAAA;AAIvB,QAAMhC,UACJ,OAAO2C,OAAMxC,UAAU,WACnBwC,MAAKxC,QACJwC,OAAMxC,OAAOH,WAAW2C,OAAM3C,WAAWoC;AAChD,SAAO,OAAOpC,YAAY,WAAWA,UAAUgC,KAAKqC,MAAM,GAAG,GAAA;AAC/D;AAVgB3B;;;AC/JT,IAAMmC,eAAN,MAAMA;EA7Cb,OA6CaA;;;;EACMC;EACAC;EAEjB,YAA6BC,SAA8B;SAA9BA,UAAAA;AAC3B,SAAKF,SAASE,QAAQF,UAAU;AAChC,SAAKC,UAAUC,QAAQC,SAASA;EAClC;EAEA,MAAMC,OACJC,OACAC,UACAJ,UAAyB,CAAC,GACN;AACpB,QAAII,SAASC,WAAW,GAAG;AACzB,aAAO,CAAA;IACT;AACA,UAAMC,OAAON,QAAQM,QAAQF,SAASC;AACtC,UAAME,YAAYH,SAASI,IAAI,CAACC,YAAYA,QAAQC,IAAI;AACxD,UAAMC,WAAW,MAAM,KAAKZ,QAAQ,KAAKC,QAAQY,KAAK;MACpDC,QAAQ;MACRC,SAAS;QACP,gBAAgB;QAChB,GAAI,KAAKd,QAAQe,SAAS;UAAEC,eAAe,UAAU,KAAKhB,QAAQe,MAAM;QAAG,IAAI,CAAC;QAChF,GAAG,KAAKf,QAAQc;MAClB;MACAG,MAAMC,KAAKC,UAAU;QACnB,GAAI,KAAKnB,QAAQoB,UAAUC,SAAY;UAAED,OAAO,KAAKpB,QAAQoB;QAAM,IAAI,CAAC;QACxEjB;QACA,GAAI,KAAKL,WAAW,QAAQ;UAAES;UAAWe,OAAOhB;QAAK,IAAI,CAAC;QAC1D,GAAI,KAAKR,WAAW,WAAW;UAAEyB,OAAOhB;QAAU,IAAI,CAAC;MACzD,CAAA;MACAiB,QAAQC,YAAYC,QAAQ,KAAK1B,QAAQ2B,aAAa,GAAA;IACxD,CAAA;AACA,UAAMV,OAAO,MAAMN,SAASD,KAAI;AAChC,QAAI,CAACC,SAASiB,IAAI;AAChB,YAAM,IAAIC,eACRlB,SAASmB,QACT,0BAA0BnB,SAASmB,MAAM,MAAMC,eAAed,IAAAA,CAAAA,EAAO;IAEzE;AACA,UAAMe,QAAQC,QAAQhB,IAAAA;AACtB,QAAIe,UAAUX,QAAW;AACvB,YAAM,IAAIQ,eAAe,KAAK,gCAAA;IAChC;AACA,UAAMK,OAAO,oBAAIC,IAAAA;AACjB,UAAMC,WAAsB,CAAA;AAC5B,eAAWC,QAAQL,OAAO;AACxB,YAAMM,QAAQD,KAAKC;AACnB,YAAM7B,UAAU,OAAO6B,UAAU,WAAWlC,SAASkC,KAAAA,IAASjB;AAC9D,UAAIZ,YAAYY,UAAaa,KAAKK,IAAID,KAAAA,GAAkB;AACtD;MACF;AACAJ,WAAKM,IAAIF,KAAAA;AACTF,eAASK,KAAK;QAAE,GAAGhC;QAASiC,OAAOC,OAAON,KAAKO,mBAAmBP,KAAKK,SAAS,CAAA;MAAG,CAAA;IACrF;AACAN,aAASS,KAAK,CAACC,GAAGC,MAAMA,EAAEL,QAAQI,EAAEJ,KAAK;AACzC,WAAON,SAASY,MAAM,GAAG1C,IAAAA;EAC3B;AACF;AAEA,SAAS2B,QAAQhB,MAAY;AAC3B,MAAIgC;AACJ,MAAI;AACFA,IAAAA,QAAO/B,KAAKgC,MAAMjC,IAAAA;EACpB,QAAQ;AACN,WAAOI;EACT;AACA,MAAI8B,MAAMC,QAAQH,KAAAA,GAAO;AACvB,WAAOA;EACT;AACA,QAAMI,UAAUJ;AAChB,QAAMjB,QAAQqB,SAASC,WAAWD,SAASE;AAC3C,SAAOJ,MAAMC,QAAQpB,KAAAA,IAAUA,QAAyBX;AAC1D;AAbSY;;;AC4BF,UAAUuB,SACfC,OACAC,OAAoC,CAAA,GAAE;AAEtC,aAAWC,QAAQF,OAAO;AACxB,UAAM;MAAEE;MAAMD;IAAK;AACnB,WAAOF,SAASG,KAAKC,UAAU;SAAIF;MAAMC;KAAK;EAChD;AACF;AARiBH;AAWV,SAASK,UACdC,MAAiC;AAEjC,QAAMC,QAAQ,oBAAIC,IAAAA;AAClB,aAAWC,SAAST,SAASM,KAAKL,KAAK,GAAG;AACxCM,UAAMG,IAAID,MAAMN,KAAKQ,IAAIF,KAAAA;EAC3B;AACA,SAAOF;AACT;AARgBF;;;ACnIhB,IAAMO,WAAW;AAGjB,IAAMC,gBAAwC;EAC5CC,MAAM;EACNC,SAAS;EACTC,OAAO;EACPC,UAAU;EACVC,OAAO;EACPC,SAAS;EACTC,UAAU;EACVC,SAAS;EACTC,SAAS;EACTC,MAAM;EACNC,SAAS;AACX;AAEA,IAAMC,UACJ;AAGF,IAAMC,WAAW;AAEjB,SAASC,MAAMX,OAAa;AAC1B,SAAOA,MACJY,QAAQ,QAAQ,GAAA,EAChBA,QAAQ,eAAe,EAAA,EACvBC,KAAI;AACT;AALSF;AAQT,SAASG,cAAcC,MAAY;AACjC,SAAO,mBAAmBC,KAAKD,IAAAA,KAASA,KAAKE,MAAM,GAAA,EAAKC,SAAS;AACnE;AAFSJ;AAKT,IAAMK,WAAW,IAAIC,IACnB,qFAAqFH,MAAM,GAAA,CAAA;AAG7F,SAASI,SAASrB,OAAa;AAC7B,MAAI,SAASgB,KAAKhB,KAAAA,GAAQ;AACxB,WAAO;EACT;AACA,QAAMsB,OAAOtB,MAAMiB,MAAM,KAAA,EAAOM,IAAG,GAAIC,YAAAA,KAAiB;AACxD,SAAOL,SAASM,IAAIH,IAAAA;AACtB;AANSD;AAiBF,SAASK,eAAeC,OAA0BC,eAAe,OAAK;AAC3E,QAAMC,WAA8B,CAAA;AACpC,MAAIC,UAAU;AAEd,MAAIC,MAA6C,CAAA;AACjD,QAAMC,UAAU,oBAAIZ,IAAAA;AACpB,QAAMa,WAAW,6BAAA;AACf,QAAIF,IAAIb,UAAU,GAAG;AACnBa,UAAIG,QAAQ,CAACC,OAAOC,aAAAA;AAClB,cAAMC,UAAUR,SAASM,MAAMG,KAAK;AAGpC,cAAMC,OACJH,aAAaL,IAAIb,SAAS,KACzBkB,aAAa,KAAKD,MAAMK,WAAWH,YAAYI,UAAaJ,QAAQK,UAAU;AACjF,YAAI,CAACH,MAAM;AACTP,kBAAQW,IAAIR,MAAMG,KAAK;QACzB;MACF,CAAA;IACF;AACAP,UAAM,CAAA;EACR,GAfiB;AAgBjBJ,QAAMO,QAAQ,CAACU,MAAMC,SAAAA;AACnB,QAAIC,WAAW;AACf,UAAMC,QAAQH,KAAK3B,MAAM,IAAA,EAAM+B,IAAI,CAACC,QAAQA,IAAIpC,KAAI,CAAA;AACpD,aAASqC,KAAK,GAAGA,KAAKH,MAAM7B,QAAQgC,MAAM;AACxC,UAAIC,OAAOJ,MAAMG,EAAAA,KAAO;AACxB,UAAIC,KAAKC,WAAW,KAAA,GAAQ;AAC1BtB,kBAAU,CAACA;AACXgB,mBAAW;AACXb,iBAAAA;AACA;MACF;AACA,UAAIkB,SAAS,IAAI;AACf;MACF;AACA,UAAIrB,WAAWqB,KAAKjC,SAAS,KAAK;AAChC4B,mBAAW;AACXb,iBAAAA;AACA;MACF;AACA,YAAMoB,cAAc,CAACP;AACrBA,iBAAW;AACX,YAAMQ,WAAW1D,SAAS2D,KAAKJ,IAAAA;AAC/B,UAAIG,WAAW,CAAA,MAAOb,UAAaa,SAAS,CAAA,MAAOb,QAAW;AAC5DR,iBAAAA;AACA,cAAMjC,QAAQW,MAAM2C,SAAS,CAAA,EAAG1C,QAAQ,SAAS,EAAA,CAAA;AACjD,YAAIZ,UAAU,IAAI;AAChB6B,mBAAS2B,KAAK;YAAExD;YAAO0C,OAAOY,SAAS,CAAA,EAAGpC;YAAQ2B;YAAMQ;UAAY,CAAA;QACtE;AACA;MACF;AACA,UAAIzB,cAAc;AAChB;MACF;AACA,YAAM6B,aAAYhD,QAAQO,KAAKmC,IAAAA,KAASzC,SAASM,KAAKmC,IAAAA;AACtD,UAAIM,YAAW;AAGb,iBAASC,SAAS,GAAGA,SAAS,GAAGA,UAAU;AACzC,gBAAMC,OAAOZ,MAAMG,KAAK,CAAA;AACxB,cAAI,CAAC,UAAUlC,KAAKmC,IAAAA,KAASQ,SAASlB,UAAakB,SAAS,MAAMA,KAAKzC,SAAS,IAAI;AAClF;UACF;AACAiC,iBAAO,GAAGA,KAAKS,MAAM,GAAG,EAAC,CAAA,GAAKD,IAAAA;AAC9BT,gBAAM;QACR;MACF;AACA,YAAMV,UAAU/B,QAAQ8C,KAAKJ,IAAAA;AAC7B,UAAIX,UAAU,CAAA,MAAOC,UAAaD,QAAQ,CAAA,MAAOC,QAAW;AAC1D,cAAM1B,OAAOyB,QAAQ,CAAA,KAAM;AAC3B,cAAMqB,UAAUrB,QAAQ,CAAA,MAAOA,QAAQ,CAAA,EAAGsB,YAAW;AACrD,YACE,CAAChD,cAAcC,IAAAA,KACf,CAAC,YAAYC,KAAKD,IAAAA,MACjBA,SAAS,MAAM8C,YAChB,CAACxC,SAASV,MAAMwC,IAAAA,CAAAA,GAChB;AACA,gBAAMT,QAAQ7C,cAAc2C,QAAQ,CAAA,EAAGhB,YAAW,CAAA,KAAO;AACzDO,cAAIyB,KAAK;YAAElB,OAAOT,SAASX;YAAQsB,SAAS;UAAK,CAAA;AACjDX,mBAAS2B,KAAK;YAAExD,OAAOW,MAAMwC,IAAAA;YAAOT;YAAOG;YAAMQ;UAAY,CAAA;AAC7D;QACF;MACF;AACA,YAAMU,WAAWrD,SAAS6C,KAAKJ,IAAAA;AAC/B,UACEY,WAAW,CAAA,MAAOtB,UAClBsB,SAAS,CAAA,MAAOtB,UAChB,CAAC3B,cAAciD,SAAS,CAAA,CAAE,KAC1B,CAAC1C,SAASV,MAAMoD,SAAS,CAAA,CAAE,CAAA,GAC3B;AAEA,cAAMrB,QAAQsB,KAAKC,IAAI,GAAGF,SAAS,CAAA,EAAG9C,MAAM,GAAA,EAAKC,SAAS,KAAK6C,SAAS,CAAA,IAAK,IAAI,EAAA;AACjFhC,YAAIyB,KAAK;UAAElB,OAAOT,SAASX;UAAQsB,SAAS;QAAM,CAAA;AAClDX,iBAAS2B,KAAK;UAAExD,OAAOW,MAAMwC,IAAAA;UAAOT;UAAOG;UAAMQ;QAAY,CAAA;AAC7D;MACF;AAIA,UAAI,CAACI,cAAa,eAAezC,KAAKmC,IAAAA,KAAS,CAACe,gBAAgBf,IAAAA,KAAS,CAACgB,YAAYhB,IAAAA,GAAO;AAC3FlB,iBAAAA;MACF;IACF;EACF,CAAA;AACAA,WAAAA;AACA,SAAOmC,uBACLC,mBACExC,SAASyC,OAAO,CAACC,GAAGjC,UAAU,CAACN,QAAQP,IAAIa,KAAAA,CAAAA,GAC3CX,MAAMT,MAAM,CAAA;AAGlB;AAhHgBQ;AAuHhB,SAAS0C,uBAAuBvC,UAA2B;AACzD,QAAMP,OAAO,oBAAIkD,IAAAA;AACjB3C,WAASK,QAAQ,CAACG,SAASC,UAAAA;AACzB,QAAI,KAAKtB,KAAKqB,QAAQrC,KAAK,GAAG;AAC5BsB,WAAKmD,IAAIpC,QAAQrC,MAAMwB,YAAW,GAAIc,KAAAA;IACxC;EACF,CAAA;AACA,SAAOT,SAASyC,OAAO,CAACjC,SAASC,UAAAA;AAC/B,UAAMoC,MAAMrC,QAAQrC,MAAMwB,YAAW;AACrC,WAAO,CAACF,KAAKG,IAAIiD,GAAAA,KAAQpD,KAAKqD,IAAID,GAAAA,MAASpC;EAC7C,CAAA;AACF;AAXS8B;AAcT,SAASD,YAAYhB,MAAY;AAC/B,MAAIA,KAAKjC,SAAS,MAAM,SAASF,KAAKmC,IAAAA,GAAO;AAC3C,WAAO;EACT;AACA,QAAMyB,SAAQzB,KAAKlC,MAAM,KAAA,EAAOqD,OAAO,CAACO,SAAS,gBAAgB7D,KAAK6D,IAAAA,CAAAA;AACtE,SACED,OAAM1D,SAAS,KAAK0D,OAAMN,OAAO,CAACO,SAAS,SAAS7D,KAAK6D,IAAAA,CAAAA,EAAO3D,SAAS0D,OAAM1D,UAAU;AAE7F;AARSiD;AAWT,SAASD,gBAAgBf,MAAY;AACnC,SACEA,KAAKjC,SAAS,MACd,sEAAsEF,KAAKmC,IAAAA;AAE/E;AALSe;AAWF,SAASY,aAAanD,OAAwB;AACnD,QAAMoD,SAAS,oBAAIP,IAAAA;AACnB,aAAW5B,QAAQjB,OAAO;AACxB,UAAMqD,OAAO,oBAAI5D,IAAAA;AACjB,eAAW6B,OAAOL,KAAK3B,MAAM,IAAA,GAAO;AAClC,YAAMyD,MAAMzB,IAAIpC,KAAI,EAAGD,QAAQ,QAAQ,GAAA,EAAKY,YAAW;AACvD,UAAIkD,OAAO,CAACM,KAAKvD,IAAIiD,GAAAA,GAAM;AACzBM,aAAKrC,IAAI+B,GAAAA;AACTK,eAAON,IAAIC,MAAMK,OAAOJ,IAAID,GAAAA,KAAQ,KAAK,CAAA;MAC3C;IACF;EACF;AACA,QAAMO,YAAYjB,KAAKkB,IAAI,GAAGlB,KAAKmB,KAAKxD,MAAMT,SAAS,GAAA,CAAA;AACvD,SAAO,IAAIE,IAAI;OAAI2D;IAAQT,OAAO,CAAC,CAAA,EAAGc,KAAAA,MAAWA,SAASH,SAAAA,EAAWjC,IAAI,CAAC,CAAC0B,GAAAA,MAASA,GAAAA,CAAAA;AACtF;AAdgBI;AAoBhB,SAAST,mBAAmBxC,UAA6BwD,WAAiB;AACxE,QAAMN,SAAS,oBAAIP,IAAAA;AACnB,aAAWnC,WAAWR,UAAU;AAC9B,UAAM6C,MAAMrC,QAAQrC,MAAMwB,YAAW;AACrCuD,WAAON,IAAIC,MAAMK,OAAOJ,IAAID,GAAAA,KAAQ,KAAK,CAAA;EAC3C;AACA,QAAMM,OAAO,oBAAI5D,IAAAA;AACjB,QAAM6D,YAAYjB,KAAKkB,IAAI,GAAGlB,KAAKmB,KAAKE,YAAY,GAAA,CAAA;AACpD,SAAOxD,SAASyC,OAAO,CAACjC,YAAAA;AACtB,UAAMqC,MAAMrC,QAAQrC,MAAMwB,YAAW;AACrC,SAAKuD,OAAOJ,IAAID,GAAAA,KAAQ,MAAMO,WAAW;AACvC,UAAID,KAAKvD,IAAIiD,GAAAA,GAAM;AACjB,eAAO;MACT;IACF;AACAM,SAAKrC,IAAI+B,GAAAA;AACT,WAAO;EACT,CAAA;AACF;AAlBSL;;;AC5JF,IAAMiB,0BAAN,cAAsCC,MAAAA;EA1F7C,OA0F6CA;;;;EAC3C,YAAqBC,OAA6D;AAChF,UAAM,8BAA8BA,KAAAA,GAAQ,GAAA,KADzBA,QAAAA;AAEnB,SAAKC,OAAO;EACd;AACF;AAGO,SAASC,eAAeC,MAAY;AACzC,SAAOC,KAAKC,KAAKF,KAAKG,SAAS,CAAA;AACjC;AAFgBJ;AAKT,IAAMK,cAAN,MAAMA;EAvGb,OAuGaA;;;;;;EACXC,QAAQ;EACRC,cAAc;EACdC,eAAe;EACfC,YAAY;EACKC;EAEjB,YACmBC,KACAC,QAIAC,aACjB;SANiBF,MAAAA;SACAC,SAAAA;SAIAC,cAAAA;AAEjB,SAAKH,WACHE,OAAOE,cAAcC,SAAYC,KAAKC,IAAG,IAAKL,OAAOE,YAAYI,OAAOC;EAC5E;;EAGAC,cAAsB;AACpB,WAAO,KAAKV,WAAWM,KAAKC,IAAG;EACjC;;EAGAI,KAAKC,QAAgBC,iBAAkC;AACrD,UAAMF,OAAO,KAAKG,SAASF,QAAQC,eAAAA,MAAqBR;AACxD,QAAI,CAACM,MAAM;AACT,WAAKZ,YAAY;IACnB;AACA,WAAOY;EACT;EAEQG,SACNF,QACAC,iBAC8C;AAC9C,QAAI,KAAKjB,SAAS,KAAKM,OAAOa,UAAU;AACtC,aAAO;IACT;AACA,QAAI,KAAKlB,cAAcP,eAAesB,MAAAA,IAAU,KAAKV,OAAOc,gBAAgB;AAC1E,aAAO;IACT;AAEA,QAAI,KAAKlB,eAAeN,KAAKyB,IAAIJ,iBAAiB,EAAA,IAAM,KAAKX,OAAOW,iBAAiB;AACnF,aAAO;IACT;AACA,QAAI,KAAKH,YAAW,KAAM,KAAK,KAAKP,aAAae,SAAS;AACxD,aAAO;IACT;AACA,WAAOb;EACT;;;;;EAMA,MAAMc,KACJC,MACAC,QACAT,QACAC,iBACiB;AACjB,UAAMzB,QAAQ,KAAK0B,SAASO,SAAST,QAAQC,eAAAA;AAC7C,QAAIzB,UAAUiB,QAAW;AACvB,WAAKN,YAAY;AACjB,YAAM,IAAIb,wBAAwBE,KAAAA;IACpC;AACA,UAAMkC,YAAY9B,KAAK+B,IACrB,IACA/B,KAAKyB,IAAIJ,iBAAiB,KAAKX,OAAOW,kBAAkB,KAAKf,YAAY,CAAA;AAE3E,UAAMM,YAAYZ,KAAKyB,IACrB,KAAKf,OAAOsB,iBAAiBhB,OAAOC,mBACpC,KAAKC,YAAW,CAAA;AAIlB,UAAMe,gBAAgBnC,eAAe+B,SAAST,MAAAA;AAC9C,SAAKf,eAAe4B;AACpB,SAAK3B,gBAAgBwB;AACrB,UAAMI,aAAa,IAAIC,gBAAAA;AACvB,UAAMC,eAAe,6BAAMF,WAAWG,MAAM,KAAK1B,aAAa2B,MAAAA,GAAzC;AACrB,SAAK3B,aAAa4B,iBAAiB,SAASH,cAAc;MAAEI,MAAM;IAAK,CAAA;AACvE,QAAIC;AACJ,SAAKrC,SAAS;AACd,QAAI;AACF,YAAMsC,WAAW,MAAMC,QAAQC,KAAK;QAClC,KAAKnC,IAAI;UAAEmB;UAAMC;UAAQT;UAAQC,iBAAiBS;UAAWe,QAAQX,WAAWW;QAAO,CAAA;QACvF,IAAIF,QAAe,CAACG,GAAGC,WAAAA;AACrB,cAAI/B,OAAOgC,SAASpC,SAAAA,GAAY;AAC9B6B,oBAAQQ,WACN,MAAA;AACEf,yBAAWG,MAAM,IAAI3C,wBAAwB,SAAA,CAAA;AAC7CqD,qBAAO,IAAIrD,wBAAwB,SAAA,CAAA;YACrC,GACAM,KAAK+B,IAAI,GAAGnB,SAAAA,CAAAA;UAEhB;QACF,CAAA;OACD;AACD,WAAKP,gBAAgBqC,SAASQ,OAAO7C,eAAe4B,iBAAiBA;AACrE,WAAK3B,iBACFoC,SAASQ,OAAO5C,gBAAgBR,eAAe4C,SAAS3C,IAAI,KAAK+B;AACpE,aAAOY,SAAS3C;IAClB,SAASoD,OAAO;AAEd,WAAK7C,gBAAgBwB;AACrB,UAAIqB,iBAAiBzD,yBAAyB;AAC5C,aAAKa,YAAY;MACnB;AACA,YAAM4C;IACR,UAAA;AACE,UAAIV,UAAU5B,QAAW;AACvBuC,qBAAaX,KAAAA;MACf;AACA,WAAK9B,aAAa0C,oBAAoB,SAASjB,YAAAA;IACjD;EACF;AACF;AAMO,SAASkB,eAAevD,MAAY;AACzC,QAAMwD,WAAW,wBAACC,eAAAA;AAChB,QAAI;AACF,YAAMC,QAAQC,KAAKC,MAAMH,UAAAA;AACzB,aAAOC,UAAU,QAAQ,OAAOA,UAAU,YAAY,CAACG,MAAMC,QAAQJ,KAAAA,IAChEA,QACD5C;IACN,QAAQ;AACN,aAAOA;IACT;EACF,GATiB;AAUjB,QAAMiD,UAAU/D,KAAKgE,KAAI;AACzB,QAAMC,SAAST,SAASO,OAAAA;AACxB,MAAIE,WAAWnD,QAAW;AACxB,WAAOmD;EACT;AACA,QAAMC,SAAS,gCAAgCC,KAAKJ,OAAAA;AACpD,MAAIG,SAAS,CAAA,MAAOpD,QAAW;AAC7B,UAAMsD,QAAQZ,SAASU,OAAO,CAAA,EAAGF,KAAI,CAAA;AACrC,QAAII,UAAUtD,QAAW;AACvB,aAAOsD;IACT;EACF;AACA,QAAMC,QAAQN,QAAQO,QAAQ,GAAA;AAC9B,QAAMC,MAAMR,QAAQS,YAAY,GAAA;AAChC,MAAIH,SAAS,KAAKE,MAAMF,OAAO;AAC7B,WAAOb,SAASO,QAAQU,MAAMJ,OAAOE,MAAM,CAAA,CAAA;EAC7C;AACA,SAAOzD;AACT;AA7BgByC;AAgCT,SAASmB,WAAWhB,OAAc;AACvC,MAAI,CAACG,MAAMC,QAAQJ,KAAAA,GAAQ;AACzB,WAAO,CAAA;EACT;AACA,SAAOA,MACJiB,IAAI,CAACC,SAAU,OAAOA,SAAS,WAAWC,OAAOD,IAAAA,IAAQA,IAAAA,EACzDE,OAAO,CAACF,SAAyB,OAAOA,SAAS,QAAA;AACtD;AAPgBF;AAShB,IAAMK,YAAwB;EAC5BC,QAAAA;EAAS;EACTT,MAAAA;EAAO;EACPU,OAAAA;EAAQ;AACV;AAMO,SAASC,yBAAyBC,UAAuB;AAC9D,SAAO,OAAOC,YAAAA;AACZ,UAAMC,SAAS,MAAMF,SAASG,QAAQ;MACpCxD,QAAQsD,QAAQtD;MAChByD,UAAU;QAAC;UAAEC,MAAM;UAAQC,SAASL,QAAQ/D;QAAO;;MACnDqE,OAAO,CAAA;MACPC,MAAMZ;MACNa,aAAaR,QAAQtC;IACvB,CAAA;AACA,WAAO;MACL9C,MAAMqF,OAAOrF;MACbmD,OAAO;QAAE7C,aAAa+E,OAAOlC,MAAM7C;QAAaC,cAAc8E,OAAOlC,MAAM5C;MAAa;IAC1F;EACF;AACF;AAdgB2E;AAsCT,SAASW,kBAAkBC,SAAiC;AACjE,QAAMC,MAAM,IAAID,QAAQE,WAAW,6BAA6BC,QAAQ,QAAQ,EAAA,CAAA;AAChF,QAAMC,UAAUJ,QAAQK,SAASA;AACjC,SAAO,OAAOf,YAAAA;AACZ,UAAMgB,QAAOhB,QAAQvD,KAAKwE,SAAS;AACnC,UAAM1D,WAAW,MAAMuD,QAAQH,KAAK;MAClCO,QAAQ;MACRC,SAAS;QACP,gBAAgB;QAChB,GAAIT,QAAQU,SAAS;UAAEC,eAAe,UAAUX,QAAQU,MAAM;QAAG,IAAI,CAAC;QACtE,GAAGV,QAAQS;MACb;MACAG,MAAM/C,KAAKgD,UAAU;QACnBC,OAAOd,QAAQc;QACfrB,UAAU;UACR;YAAEC,MAAM;YAAUC,SAASL,QAAQtD;UAAO;UAC1C;YAAE0D,MAAM;YAAQC,SAASL,QAAQ/D;UAAO;;QAE1CwF,aAAaf,QAAQe,eAAe;QACpCC,YAAY1B,QAAQ9D;QACpB,GAAIwE,QAAQiB,YAAYX,QAAO;UAAEY,iBAAiB;YAAEC,MAAM;UAAc;QAAE,IAAI,CAAC;QAC/E,GAAGnB,QAAQY;MACb,CAAA;MACA5D,QAAQsC,QAAQtC;IAClB,CAAA;AACA,UAAM4D,OAAO,MAAM/D,SAAS3C,KAAI;AAChC,QAAI,CAAC2C,SAASuE,IAAI;AAChB,YAAM,IAAIC,eACRxE,SAASyE,QACT,2BAA2BzE,SAASyE,MAAM,MAAMC,eAAeX,IAAAA,CAAAA,EAAO;IAE1E;AACA,UAAMY,SAAS/D,eAAemD,IAAAA;AAM9B,UAAMjB,UAAU6B,QAAQC,UAAU,CAAA,GAAIC,SAAS/B;AAC/C,QAAI,OAAOA,YAAY,UAAU;AAC/B,YAAM,IAAI0B,eAAe,KAAK,6CAAA;IAChC;AACA,UAAMhE,QAA+C,CAAC;AACtD,QAAI,OAAOmE,QAAQnE,OAAOsE,kBAAkB,UAAU;AACpDtE,YAAM7C,cAAcgH,OAAOnE,MAAMsE;IACnC;AACA,QAAI,OAAOH,QAAQnE,OAAOuE,sBAAsB,UAAU;AACxDvE,YAAM5C,eAAe+G,OAAOnE,MAAMuE;IACpC;AACA,WAAO;MAAE1H,MAAMyF;MAAStC;IAAM;EAChC;AACF;AAnDgB0C;AAgET,SAAS8B,cAAcjH,KAAckH,QAAsB,oBAAIC,IAAAA,GAAK;AACzE,SAAO,OAAOzC,YAAAA;AACZ,UAAM0C,MAAMC,WACV,GAAG3C,QAAQvD,KAAKwE,IAAI,KAASjB,QAAQ9D,eAAe,KAAS8D,QAAQtD,MAAM,KAASsD,QAAQ/D,MAAM,EAAE;AAEtG,UAAM2G,MAAM,MAAMJ,MAAMK,IAAIH,GAAAA;AAC5B,QAAIE,QAAQlH,QAAW;AACrB,aAAO;QAAEd,MAAMgI,IAAIhI;QAAMmD,OAAO;UAAE7C,aAAa;UAAGC,cAAc;QAAE;MAAE;IACtE;AACA,UAAMoC,WAAW,MAAMjC,IAAI0E,OAAAA;AAC3B,UAAMwC,MAAMM,IAAIJ,KAAKnF,QAAAA;AACrB,WAAOA;EACT;AACF;AAbgBgF;AAmBT,SAASI,WAAW/H,MAAY;AACrC,MAAImI,KAAK;AACT,MAAIC,KAAK,WAAapI,KAAKG;AAC3B,WAASkI,IAAI,GAAGA,IAAIrI,KAAKG,QAAQkI,KAAK;AACpC,UAAMC,IAAItI,KAAKuI,WAAWF,CAAAA;AAC1BF,SAAKlI,KAAKuI,KAAKL,KAAKG,GAAG,QAAA;AACvBF,SAAKnI,KAAKuI,KAAKJ,KAAKE,GAAG,UAAA;AACvBF,UAAMA,OAAO;EACf;AACAD,OAAKlI,KAAKuI,KAAKL,KAAMA,OAAO,IAAK,UAAA;AACjCC,OAAKnI,KAAKuI,KAAKJ,KAAMA,OAAO,IAAK,UAAA;AACjC,UAAQD,OAAO,GAAGM,SAAS,EAAA,EAAIC,SAAS,GAAG,GAAA,KAAQN,OAAO,GAAGK,SAAS,EAAA,EAAIC,SAAS,GAAG,GAAA;AACxF;AAZgBX;;;AChUhB,IAAMY,iBAAiB;EACrBC,UAAU;EACVC,gBAAgB;EAChBC,iBAAiB;EACjBC,WAAW;EACXC,eAAe;AACjB;AAEA,IAAMC,mBACJ;AACF,IAAMC,iBACJ;AAGK,SAASC,QAAQC,OAAwB;AAK9C,MAAIA,MAAMC,UAAUC,UAAaF,MAAMG,aAAaD,QAAW;AAC7D,UAAM,IAAIE,MAAM,yDAAA;EAClB;AACA,MAAIJ,MAAMG,aAAaD,QAAW;AAChC,UAAMG,QAAQL,MAAMG,SAASG,IAAI,CAACC,SAASC,WAAW;MACpDA;MACAC,MAAMF,QAAQE;MACd,GAAIF,QAAQG,SAASR,SAAY;QAAEQ,MAAMH,QAAQG;MAAK,IAAI,CAAC;IAC7D,EAAA;AACA,UAAMC,SAASX,MAAMG,SAASS,KAAK,CAACL,YAAYA,QAAQM,OAAOC,KAAAA,CAAAA;AAC/D,UAAMC,WAAWJ,SACbX,MAAMG,SAASa,QAAQ,CAACT,SAASU,SAC/BV,QAAQM,OAAOC,KAAAA,IACX;MACE;QACED,OAAON,QAAQM,MAAMC,KAAI;QACzBI,OAAOC,KAAKC,IAAI,GAAGD,KAAKE,IAAI,GAAGd,QAAQW,SAAS,CAAA,CAAA;QAChDD;QACAK,aAAa;MACf;QAEF,CAAA,CAAE,IAERpB;AACJ,WAAO;MAAEG;MAAOU;MAAUQ,MAAM;IAAW;EAC7C;AACA,QAAMtB,SAAQD,MAAMC,SAAS,CAAA;AAC7B,SAAO;IACLI,OAAOJ,OAAMK,IAAI,CAACG,MAAMD,WAAW;MAAEA;MAAOC;MAAMC,MAAMF,QAAQ;IAAE,EAAA;IAClEO,UAAUb;IACVqB,MAAM;EACR;AACF;AArCgBxB;AAuChB,SAASyB,gBAAgBxB,OAA0BK,OAAiB;AAClE,QAAMoB,WAA8B,CAAA;AACpC,aAAWC,SAAS1B,MAAM2B,WAAW,CAAA,GAAI;AACvC,QAAIV,OAAOS,MAAMT;AACjB,QAAIA,SAASf,UAAawB,MAAMhB,SAASR,QAAW;AAClDe,aAAOZ,MAAMuB,UAAU,CAACC,eAAcA,WAAUnB,SAASgB,MAAMhB,IAAI;AACnE,UAAIO,OAAO,GAAG;AACZA,eAAOS,MAAMhB,OAAO;MACtB;IACF;AACA,QAAIO,SAASf,UAAae,OAAO,KAAKA,QAAQZ,MAAMyB,UAAU,CAACJ,MAAMb,MAAMC,KAAI,GAAI;AACjF;IACF;AACAW,aAASM,KAAK;MACZlB,OAAOa,MAAMb,MAAMC,KAAI;MACvBI,OAAOC,KAAKC,IAAI,GAAGD,KAAKE,IAAI,GAAGF,KAAKa,MAAMN,MAAMR,KAAK,CAAA,CAAA;MACrDD;MACAK,aAAa;IACf,CAAA;EACF;AAEA,SAAOG,SAASQ,KAAK,CAACC,GAAGC,MAAMD,EAAEjB,OAAOkB,EAAElB,IAAI;AAChD;AAtBSO;AAiCT,SAASY,aAAaX,UAA6BY,WAAiB;AAClE,QAAMC,QAAqB,CAAA;AAC3B,QAAMC,QAA8C,CAAA;AACpDd,WAASe,QAAQ,CAACC,SAASjC,UAAAA;AAEzB,QAAIkC,MAAML,YAAY;AACtB,aAASM,OAAOnC,QAAQ,GAAGmC,OAAOlB,SAASK,QAAQa,QAAQ;AACzD,YAAMd,aAAYJ,SAASkB,IAAAA;AAC3B,UAAId,eAAc3B,UAAa2B,WAAUX,SAASuB,QAAQvB,OAAO;AAC/DwB,cAAMb,WAAUP,cAAcO,WAAUZ,OAAO,IAAIY,WAAUZ;AAC7D;MACF;IACF;AACA,UAAM2B,OAAkB;MACtB/B,OAAO4B,QAAQ5B;MACfgC,WAAWJ,QAAQxB;MACnB6B,SAAS3B,KAAKC,IAAIqB,QAAQxB,MAAMyB,GAAAA;MAChCK,UAAU,CAAA;IACZ;AACA,WAAOR,MAAMT,SAAS,MAAMS,MAAMA,MAAMT,SAAS,CAAA,GAAIZ,SAAS,MAAMuB,QAAQvB,OAAO;AACjFqB,YAAMS,IAAG;IACX;AACA,UAAMC,SAASV,MAAMA,MAAMT,SAAS,CAAA;AACpC,QAAImB,WAAW/C,QAAW;AACxBoC,YAAMP,KAAKa,IAAAA;IACb,OAAO;AACLK,aAAOL,KAAKG,SAAShB,KAAKa,IAAAA;AAC1BK,aAAOL,KAAKE,UAAU3B,KAAKC,IAAI6B,OAAOL,KAAKE,SAASF,KAAKE,OAAO;IAClE;AACAP,UAAMR,KAAK;MAAEa;MAAM1B,OAAOuB,QAAQvB;IAAM,CAAA;EAC1C,CAAA;AAEA,QAAMgC,QAAQZ,MAAM,CAAA;AACpB,MAAIY,UAAUhD,UAAagD,MAAML,YAAY,GAAG;AAC9CP,UAAMa,QAAQ;MACZtC,OAAO;MACPgC,WAAW;MACXC,SAASI,MAAML,YAAY;MAC3BE,UAAU,CAAA;IACZ,CAAA;EACF;AACA,SAAOT;AACT;AA1CSF;AA6CT,SAASgB,UAAU3C,MAAc4C,SAA4B;AAC3D,QAAMC,OAAO7C,KACV8C,MAAM,IAAA,EACNjD,IAAI,CAACuB,eAAcA,WAAUf,KAAI,CAAA,EACjC0C,KACC,CAAC3B,eACC,eAAe4B,KAAK5B,UAAAA,KACpB,CAACwB,QAAQK,IAAI7B,WAAU8B,QAAQ,QAAQ,GAAA,EAAKC,YAAW,CAAA,CAAA;AAE7D,SAAON,SAASpD,SAAY,KAAKoD,KAAKxB,SAAS,KAAK,GAAGwB,KAAKO,MAAM,GAAG,EAAA,CAAA,WAASP;AAChF;AAVSF;AAYT,SAASU,WAAWzD,OAAmB0D,OAAerB,KAAW;AAC/D,QAAMQ,QAAQ7C,MAAM0D,KAAAA;AACpB,QAAMC,OAAO3D,MAAMqC,GAAAA;AACnB,MAAIQ,OAAOxC,SAASR,UAAa8D,MAAMtD,SAASR,QAAW;AACzD,WAAOgD,MAAMxC,SAASsD,KAAKtD,OAAO,QAAQwC,MAAMxC,IAAI,KAAK,SAASwC,MAAMxC,IAAI,SAAIsD,KAAKtD,IAAI;EAC3F;AACA,SAAOqD,UAAUrB,MAAM,WAAWqB,QAAQ,CAAA,KAAM,YAAYA,QAAQ,CAAA,SAAKrB,MAAM,CAAA;AACjF;AAPSoB;AAUT,SAASG,WACP5D,OACA0D,OACArB,KACAwB,WACAC,aACAC,YACAf,SAA4B;AAE5B,MAAInC,QAAqB,CAAA;AACzB,WAASmD,OAAON,OAAOM,QAAQ3B,KAAK2B,QAAQH,WAAW;AACrD,UAAMI,KAAKnD,KAAKE,IAAIqB,KAAK2B,OAAOH,YAAY,CAAA;AAC5C,UAAMK,OAAOnB,UAAU/C,MAAMgE,IAAAA,GAAO5D,QAAQ,IAAI4C,OAAAA;AAChDnC,UAAMa,KAAK;MACTlB,OAAO,GAAGiD,WAAWzD,OAAOgE,MAAMC,EAAAA,CAAAA,GAAMC,OAAO,KAAKA,IAAAA,KAAS,EAAA;MAC7D1B,WAAWwB;MACXvB,SAASwB;MACTvB,UAAU,CAAA;IACZ,CAAA;EACF;AACA,MAAIyB,QAAQ;AACZ,SAAOtD,MAAMY,SAASqC,eAAeK,QAAQJ,YAAY;AACvD,UAAMK,UAAuB,CAAA;AAC7B,UAAMC,MAAMvD,KAAKC,IAAI+C,aAAahD,KAAKwD,KAAKzD,MAAMY,SAASqC,WAAAA,CAAAA;AAC3D,aAAS3D,QAAQ,GAAGA,QAAQU,MAAMY,QAAQtB,SAASkE,KAAK;AACtD,YAAM3B,WAAW7B,MAAM2C,MAAMrD,OAAOA,QAAQkE,GAAAA;AAC5C,YAAML,OAAOtB,SAAS,CAAA,GAAIF,aAAakB;AACvC,YAAMO,KAAKvB,SAASA,SAASjB,SAAS,CAAA,GAAIgB,WAAWJ;AACrD+B,cAAQ1C,KAAK;QAAElB,OAAOiD,WAAWzD,OAAOgE,MAAMC,EAAAA;QAAKzB,WAAWwB;QAAMvB,SAASwB;QAAIvB;MAAS,CAAA;IAC5F;AACA7B,YAAQuD;AACRD,aAAS;EACX;AACA,SAAOtD;AACT;AAlCS+C;AAqCT,SAASW,iBAAiBC,OAAoBC,OAAiB,CAAA,GAAE;AAC/D,aAAW,EAAElC,KAAI,KAAMmC,UAAUF,KAAAA,GAAQ;AACvC,QAAIC,KAAKhD,SAAS,IAAI;AACpBgD,WAAK/C,KAAKa,KAAK/B,KAAK;IACtB;EACF;AACA,SAAOiE;AACT;AAPSF;AAkBT,SAASI,MACPH,OACAxE,OACAmE,OACAS,SAMC;AAED,QAAMC,SAASL,MAAMvE,IAAI,CAACsC,SAAAA;AACxB,QACE4B,SAASS,QAAQE,YAChBvC,KAAKC,cAAcD,KAAKE,WAAWF,KAAKG,SAASjB,SAAS,GAC3D;AACA,YAAMsD,WAAWR,iBAAiBhC,KAAKG,UAAU;WAAKH,KAAKwC,YAAY,CAAA;OAAI;AAC3E,aAAO;QACL,GAAGxC;QACH,GAAIwC,SAAStD,SAAS,IAAI;UAAEsD;QAAS,IAAI,CAAC;QAC1CrC,UAAU,CAAA;MACZ;IACF;AACA,QAAIA,WAAWH,KAAKG;AACpB,UAAMsC,OAAOtC,SAAS,CAAA;AACtB,QACEA,SAASjB,WAAW,KACpBuD,SAASnF,UACTmF,KAAKxC,cAAcD,KAAKC,aACxBwC,KAAKvC,YAAYF,KAAKE,SACtB;AACAC,iBAAWsC,KAAKtC;IAClB;AACA,QAAIA,SAASjB,WAAW,KAAKc,KAAKE,UAAUF,KAAKC,YAAY,IAAIoC,QAAQK,cAAc;AACrFvC,iBAAWkB,WACT5D,OACAuC,KAAKC,WACLD,KAAKE,SACLmC,QAAQf,WACRe,QAAQd,aACRc,QAAQE,WAAWX,OACnBS,QAAQ5B,OAAO;IAEnB;AACA,WAAO;MAAE,GAAGT;MAAMG,UAAUiC,MAAMjC,UAAU1C,OAAOmE,QAAQ,GAAGS,OAAAA;IAAS;EACzE,CAAA;AACA,QAAMM,SAAsB,CAAA;AAC5B,aAAW3C,QAAQsC,QAAQ;AACzB,UAAMM,WAAWD,OAAOA,OAAOzD,SAAS,CAAA;AACxC,QACE0D,aAAatF,UACbsF,SAASzC,SAASjB,WAAW,KAC7Bc,KAAKG,SAASjB,WAAW,KACzB0D,SAAS3C,cAAcD,KAAKC,aAC5B2C,SAAS1C,YAAYF,KAAKE,SAC1B;AACA0C,eAASJ,WAAW;WACdI,SAASJ,YAAY,CAAA;QACzBxC,KAAK/B;WACD+B,KAAKwC,YAAY,CAAA;QACrBvB,MAAM,GAAG,EAAA;AACX;IACF;AACA0B,WAAOxD,KAAKa,IAAAA;EACd;AACA,SAAO2C;AACT;AAnESP;AAqET,UAAUD,UAAUF,OAAkB;AACpC,aAAWjC,QAAQiC,OAAO;AACxB,UAAM;MAAEjC;IAAK;AACb,WAAOmC,UAAUnC,KAAKG,QAAQ;EAChC;AACF;AALUgC;AAQH,SAASU,SAASpF,OAA4B0D,OAAerB,KAAW;AAC7E,QAAMgD,QAAkB,CAAA;AACxB,WAASlF,QAAQW,KAAKC,IAAI,GAAG2C,KAAAA,GAAQvD,SAASkC,OAAOlC,QAAQH,MAAMyB,QAAQtB,SAAS;AAClFkF,UAAM3D,KAAK1B,MAAMG,KAAAA,GAAQC,QAAQ,EAAA;EACnC;AACA,SAAOiF,MAAMC,KAAK,MAAA;AACpB;AANgBF;AAahB,SAASG,SAASvF,OAA4BuC,MAAsB;AAClE,QAAMM,QAAQN,KAAKG,SAAS,CAAA;AAC5B,QAAML,MAAMQ,UAAUhD,SAAY0C,KAAKE,UAAU3B,KAAKC,IAAIwB,KAAKC,WAAWK,MAAML,YAAY,CAAA;AAC5F,QAAMgD,QAAkB,CAAA;AACxB,WAASrF,QAAQoC,KAAKC,WAAWrC,SAASkC,OAAOlC,QAAQH,MAAMyB,QAAQtB,SAAS;AAC9EqF,UAAM9D,KAAK1B,MAAMG,KAAAA,GAAQC,QAAQ,EAAA;EACnC;AACA,QAAMqF,OAAOD,MAAM,CAAA;AACnB,MAAIC,SAAS5F,QAAW;AACtB,UAAM6F,QAAQnD,KAAK/B,MAAMgD,MAAM,GAAG,EAAA;AAClC,UAAMmC,KAAKD,MAAMjE,UAAU,IAAIgE,KAAKG,QAAQF,KAAAA,IAAS;AACrD,QAAIC,KAAK,GAAG;AACVH,YAAM,CAAA,IAAKC,KAAKjC,MAAMmC,EAAAA;IACxB;EACF;AACA,SAAOH;AACT;AAhBSD;AAsBT,SAASM,UAAUL,OAA0BzE,KAAW;AACtD,QAAM+E,QAAQN,MAAMO,OAAO,CAACC,KAAK5F,SAAS4F,MAAM5F,KAAKqB,QAAQ,CAAA;AAC7D,MAAIqE,SAAS/E,KAAK;AAChB,WAAOyE,MAAMF,KAAK,MAAA;EACpB;AACA,QAAMjB,MAAMvD,KAAKC,IAAI,KAAKD,KAAKa,MAAMZ,MAAMyE,MAAM/D,MAAM,CAAA;AACvD,QAAM4D,QAAkB,CAAA;AACxB,MAAIY,OAAO;AACX,aAAW7F,QAAQoF,OAAO;AACxB,QAAIS,QAAQlF,KAAK;AACf;IACF;AACA,UAAMmF,OAAO9F,KAAKqB,SAAS4C,MAAM,GAAGjE,KAAKoD,MAAM,GAAGa,GAAAA,CAAAA,WAAUjE;AAC5DiF,UAAM3D,KAAKwE,IAAAA;AACXD,YAAQC,KAAKzE;EACf;AACA,SAAO4D,MAAMC,KAAK,MAAA;AACpB;AAjBSO;AAmBT,SAASM,SAAS/F,MAAcW,KAAW;AACzC,QAAMqF,OAAOhG,KAAKkD,QAAQ,QAAQ,GAAA,EAAK7C,KAAI;AAC3C,SAAO2F,KAAK3E,SAASV,MAAM,GAAGqF,KAAK5C,MAAM,GAAGzC,MAAM,CAAA,CAAA,WAAQqF;AAC5D;AAHSD;AAMT,eAAeE,YACbC,KACAtG,OACA4E,SAAkD;AAElD,QAAM2B,UAAUzF,KAAKC,IAAI,KAAKD,KAAKa,MAAMiD,QAAQ4B,cAAc,CAAA,CAAA;AAC/D,QAAMC,UAAwB,CAAA;AAC9B,MAAIC,UAAsB,CAAA;AAC1B,MAAIC,OAAO;AACX,aAAW/F,QAAQZ,OAAO;AACxB,UAAMyB,SAASX,KAAKE,IAAIJ,KAAKR,KAAKqB,QAAQ8E,OAAAA;AAC1C,QAAIG,QAAQjF,SAAS,KAAKkF,OAAOlF,SAASmD,QAAQ4B,aAAa;AAC7DC,cAAQ/E,KAAKgF,OAAAA;AACbA,gBAAU,CAAA;AACVC,aAAO;IACT;AACAD,YAAQhF,KAAKd,IAAAA;AACb+F,YAAQlF;EACV;AACA,MAAIiF,QAAQjF,SAAS,GAAG;AACtBgF,YAAQ/E,KAAKgF,OAAAA;EACf;AACA,MAAID,QAAQhF,SAASmD,QAAQzF,UAAU;AACrC,UAAM,IAAIY,MACR,kBAAkB0G,QAAQhF,MAAM,oDAAoDmD,QAAQzF,QAAQ,GAAG;EAE3G;AACA,QAAMiC,WAA8B,CAAA;AACpC,aAAWwF,UAAUH,SAAS;AAC5B,UAAMI,QAAQD,OAAO3G,IAAI,CAACW,UAAU;MAClCA,MAAMA,KAAKT;MACX,GAAIS,KAAKP,SAASR,SAAY;QAAEQ,MAAMO,KAAKP;MAAK,IAAI,CAAC;MACrDD,MAAMQ,KAAKR,KAAKqB,SAAS8E,UAAU,GAAG3F,KAAKR,KAAKoD,MAAM,GAAG+C,OAAAA,CAAAA,WAAc3F,KAAKR;IAC9E,EAAA;AACA,UAAM0G,OAAOD,MACV5G,IAAI,CAACW,SAAS,SAASA,KAAKA,IAAI;EAAMA,KAAKR,IAAI;SAAYQ,KAAKA,IAAI,GAAG,EACvE0E,KAAK,IAAA;AACR,UAAMyB,SAAS;;;;EAIjBD,IAAAA;;;AAGE,UAAME,QAAQC,eACZ,MAAMX,IAAIY,KAAK;MAAEhG,MAAM;MAAalB,OAAO6G;IAAM,GAAGrH,kBAAkBuH,QAAQ,GAAA,CAAA;AAEhF,UAAMjH,WAAWkH,OAAOlH;AACxB,QAAI,CAACqH,MAAMC,QAAQtH,QAAAA,GAAW;AAC5B,YAAM,IAAIC,MAAM,0CAAA;IAClB;AACA,UAAM8C,QAAQ+D,OAAO,CAAA,GAAIzG,SAAS;AAClC,UAAMwD,OAAOiD,OAAOA,OAAOnF,SAAS,CAAA,GAAItB,SAAS0C;AACjD,eAAWxB,SAASvB,UAAU;AAC5B,UAAIuB,UAAU,QAAQ,OAAOA,UAAU,UAAU;AAC/C;MACF;AACA,YAAM,EAAEb,OAAOK,OAAOD,KAAI,IAAKS;AAC/B,YAAMsE,KAAK,OAAO/E,SAAS,WAAWA,OAAOyG,OAAOzG,IAAAA;AACpD,UACE,OAAOJ,UAAU,YACjB,CAACA,MAAMC,KAAI,KACX,CAAC4G,OAAOC,UAAU3B,EAAAA,KAClBA,KAAK9C,SACL8C,KAAKhC,MACL;AACA;MACF;AACA,YAAMwB,WAAW/D,SAASA,SAASK,SAAS,CAAA;AAC5C,UAAI0D,aAAatF,UAAa8F,KAAKR,SAASvE,MAAM;AAChD;MACF;AACAQ,eAASM,KAAK;QACZlB,OAAO2F,SAAS3F,OAAO,GAAA;QACvBK,OAAOC,KAAKC,IAAI,GAAGD,KAAKE,IAAI,GAAGF,KAAKa,MAAM,OAAOd,UAAU,WAAWA,QAAQ,CAAA,CAAA,CAAA;QAC9ED,MAAM+E;QACN1E,aAAa;MACf,CAAA;IACF;EACF;AACA,SAAOG;AACT;AAjFeiF;AAoFf,eAAsBkB,SACpBC,OACAC,OACAC,MAA4C;AAE5C,QAAMC,UAAU,IAAIR,MAASK,MAAM/F,MAAM;AACzC,MAAIa,OAAO;AACX,QAAMsF,UAAUT,MAAMnD,KAAK;IAAEvC,QAAQX,KAAKC,IAAI,GAAGD,KAAKE,IAAIyG,OAAOD,MAAM/F,MAAM,CAAA;EAAG,GAAG,YAAA;AACjF,WAAOa,OAAOkF,MAAM/F,QAAQ;AAC1B,YAAMtB,QAAQmC;AACdqF,cAAQxH,KAAAA,IAAS,MAAMuH,KAAKF,MAAMrH,KAAAA,GAAaA,KAAAA;IACjD;EACF,CAAA;AACA,QAAM0H,QAAQC,IAAIF,OAAAA;AAClB,SAAOD;AACT;AAfsBJ;AAqCtB,eAAsBQ,kBACpBpI,OACAiF,SAAiC;AAEjC,QAAMoD,UAAUC,KAAKC,IAAG;AACxB,QAAM,EAAElI,OAAOU,UAAUQ,KAAI,IAAKxB,QAAQC,KAAAA;AAC1C,QAAMmF,WAAWhE,KAAKC,IAAI,GAAG6D,QAAQE,YAAY,CAAA;AACjD,QAAMjB,YAAY/C,KAAKC,IAAI,GAAG6D,QAAQf,aAAa,CAAA;AACnD,QAAMoB,eAAenE,KAAKC,IAAI8C,WAAWe,QAAQK,gBAAgB,EAAA;AACjE,QAAMnB,cAAchD,KAAKC,IAAI,GAAG6D,QAAQd,eAAe,EAAA;AACvD,QAAMqE,cAAcrH,KAAKC,IAAI,GAAG6D,QAAQuD,eAAe,CAAA;AACvD,QAAMC,aAAaxD,QAAQ0B,QAAQzG;AACnC,QAAMwI,YAAYzD,QAAQyD,aAAaD;AACvC,QAAME,WAAW1D,QAAQ0D,YAAYF;AACrC,QAAMG,eAAe3D,QAAQ2D,gBAAgBH;AAC7C,QAAMI,SAAS5D,QAAQ6D,kBAAkB;AACzC,QAAMC,kBAAkB9D,QAAQ8D,mBAAmB;AACnD,QAAMC,oBAAoB/D,QAAQ+D,qBAAqB;AAEvD,QAAMC,aAAa5I,MAAMC,IAAI,CAACW,SAASiI,WAAWjI,KAAKR,IAAI,CAAA;AAC3D,QAAM0I,cAAcD,WAClBE,KAAKC,UAAU;IACbC,GAAG;IACH/H;IACAV,OAAOb,MAAMa,SAAS;IACtBc,SAAS3B,MAAM2B,WAAW;IAC1BxB,UACEH,MAAMG,UAAUG,IAAI,CAACC,YAAY;MAC/BA,QAAQM,SAAS;MACjBN,QAAQW,SAAS;MACjBX,QAAQG,QAAQ;KACjB,KAAK;IACRL,OAAO4I;IACP9D;IACAjB;IACAoB;IACAnB;IACAqE;IACAK;IACAH;IACAC;IACAC,cAAcA,gBAAgBH;IAC9BM;IACAC;EACF,CAAA,CAAA;AAEF,MAAI/D,QAAQO,UAAU2D,gBAAgBA,aAAa;AAEjD,UAAM,EAAEI,UAAUC,WAAWC,QAAQC,SAAS,GAAGlE,SAAAA,IAAaP,QAAQO;AACtE,UAAM+D,WAAWtE,QAAQsE,YAAYtE,QAAQO,SAAS+D;AACtD,UAAME,SAASxE,QAAQwE,UAAUxE,QAAQO,SAASiE;AAClD,WAAO;MACLE,MAAM;QACJ,GAAGnE;QACH,GAAI+D,aAAarJ,SAAY;UAAEqJ;QAAS,IAAI,CAAC;QAC7C,GAAIE,WAAWvJ,SAAY;UAAEuJ;QAAO,IAAI,CAAC;MAC3C;MACApJ;MACAuJ,WAAW;IACb;EACF;AAEA,QAAMC,SAAS,IAAIC,YACjB7E,QAAQ0B,QAAQ,MAAMuB,QAAQ6B,OAAO,IAAI3J,MAAM,QAAA,CAAA,IAC/C;IAAE,GAAGb;IAAgB,GAAG0F,QAAQ4E;EAAO,GACvC5E,QAAQ+E,MAAM;AAEhB,MAAIC;AAGJ,MAAIC,YAAiC;AACrC,MAAIzI,WAA8B,CAAA;AAClC,QAAM4B,UAAU8G,aAAa9J,MAAMC,IAAI,CAACW,SAASA,KAAKR,IAAI,CAAA;AAC1D,QAAMkB,UAAUH,gBAAgBxB,OAAOK,KAAAA;AACvC,MAAIsB,QAAQG,UAAUX,KAAKE,IAAImH,aAAa,CAAA,GAAI;AAC9C0B,gBAAY;AACZzI,eAAWE;EACb,WAAWZ,aAAab,UAAaa,SAASe,UAAU,GAAG;AACzDoI,gBAAY;AACZzI,eAAWV;EACb,WAAW8H,WAAW,SAASxI,MAAMyB,SAAS,GAAG;AAC/C,UAAMsI,QAAQtB,eACZzI,MAAMC,IAAI,CAACW,SAASA,KAAKR,IAAI,GAC7BoI,WAAW,UAAA;AAEb,QAAIuB,MAAMtI,UAAU0G,aAAa;AAC/B0B,kBAAY;AACZzI,iBAAW2I;IACb;EACF;AACA,MAAIF,cAAc,UAAUtB,gBAAgBH,cAAcpI,MAAMyB,SAAS,GAAG;AAC1E,QAAI;AACF,YAAMsI,QAAQ,MAAM1D,YAAYmD,QAAQxJ,OAAO;QAC7CwG,aAAa5B,QAAQoF,wBAAwB;QAC7C7K,UAAU2B,KAAKC,IAAI,GAAG6D,QAAQqF,qBAAqB,EAAA;MACrD,CAAA;AACA,UAAIF,MAAMtI,UAAUX,KAAKE,IAAImH,aAAa,CAAA,GAAI;AAC5C0B,oBAAY;AACZzI,mBAAW2I;MACb,OAAO;AACLH,yBAAiB;MACnB;IACF,SAASM,OAAO;AACdN,uBACEM,iBAAiBC,0BACb,4BAA4BD,MAAME,OAAO,KACzC,2BAA4BF,MAAgBE,OAAO;IAC3D;EACF;AAEA,QAAMC,QACJR,cAAc,SACV7J,MAAMyB,WAAW,IACf,CAAA,IACAmC,WAAW5D,OAAO,GAAGA,MAAMyB,SAAS,GAAGoC,WAAWC,aAAagB,UAAU9B,OAAAA,IAC3EjB,aAAaX,UAAUpB,MAAMyB,MAAM;AACzC,QAAMoD,SAASF,MAAM0F,OAAOrK,OAAO,GAAG;IACpC8E;IACAG;IACApB;IACAC;IACAd;EACF,CAAA;AAGA,MAAIsH,UAAU;AACd,QAAMC,WAAW,wBAAC/F,WAChBA,OAAMvE,IAAI,CAACsC,SAAAA;AACT+H,eAAW;AACX,UAAME,KAAKC,OAAOH,OAAAA,EAASI,SAAS,GAAG,GAAA;AACvC,UAAMC,YAAY3K,MAAMuC,KAAKC,SAAS,GAAGnC;AACzC,UAAMuK,UAAU5K,MAAMuC,KAAKE,OAAO,GAAGpC;AACrC,WAAO;MACLmK;MACAhK,OAAO+B,KAAK/B;MACZgC,WAAWD,KAAKC;MAChBC,SAASF,KAAKE;MACd,GAAIkI,cAAc9K,SAAY;QAAE8K;MAAU,IAAI,CAAC;MAC/C,GAAIC,YAAY/K,SAAY;QAAE+K;MAAQ,IAAI,CAAC;MAC3C,GAAIrI,KAAKwC,aAAalF,SAAY;QAAEkF,UAAUxC,KAAKwC;MAAS,IAAI,CAAC;MACjE8F,aAAahC,WACX,GAAGtG,KAAK/B,KAAK,KAASoI,WAAWpF,MAAMjB,KAAKC,WAAWD,KAAKE,UAAU,CAAA,EAAG6C,KAAK,GAAA,CAAA,EAAM;MAEtF5C,UAAU6H,SAAShI,KAAKG,QAAQ;IAClC;EACF,CAAA,GAnBe;AAoBjB,QAAM8B,QAAQ+F,SAAS1F,MAAAA;AAGvB,QAAMiG,oBAAoB,oBAAIC,IAAAA;AAC9B,MAAInG,QAAQO,aAAatF,QAAW;AAClC,eAAW,EAAE0C,KAAI,KAAMyI,SAASpG,QAAQO,SAASX,KAAK,GAAG;AACvD,UACEjC,KAAK0I,YAAYpL,WAChB0C,KAAK2I,kBAAkB,SAAS3I,KAAK2I,kBAAkB,WACxD;AACAJ,0BAAkBK,IAAI5I,KAAKsI,aAAatI,KAAK0I,OAAO;MACtD;IACF;EACF;AACA,QAAMG,eAAmC,CAAA;AACzC,WAASC,QAAQ;OAAI7G;KAAQ6G,MAAM5J,SAAS,KAAK;AAC/C,UAAMc,OAAO8I,MAAMC,MAAK;AACxBF,iBAAa1J,KAAKa,IAAAA;AAClB8I,YAAQA,MAAME,OAAOhJ,KAAKG,QAAQ;EACpC;AACA,MAAI8I,kBAAkB;AACtB,QAAMC,UAA8B,CAAA;AACpC,aAAWlJ,QAAQ6I,cAAc;AAC/B,UAAMM,SAASZ,kBAAkBa,IAAIpJ,KAAKsI,WAAW;AACrD,UAAMe,MAAMrG,SAASvF,OAAOuC,IAAAA,EAAM+C,KAAK,MAAA;AACvC,QAAIoG,WAAW7L,QAAW;AACxB0C,WAAK0I,UAAUS;AACfnJ,WAAK2I,gBAAgB;AACrBM,yBAAmB;IACrB,WAAWI,IAAInL,KAAI,EAAGgB,SAASiH,mBAAmBnG,KAAKG,SAASjB,WAAW,GAAG;AAC5E,YAAMrB,OAAO+F,SAASyF,KAAKlD,eAAAA;AAC3B,UAAItI,MAAM;AACRmC,aAAK0I,UAAU7K;AACfmC,aAAK2I,gBAAgB;MACvB;IACF,WAAW7C,aAAaD,YAAY;AAClCqD,cAAQ/J,KAAKa,IAAAA;IACf,OAAO;AACL,YAAMnC,OAAO+F,SAASyF,KAAK,GAAA;AAC3B,UAAIxL,MAAM;AACRmC,aAAK0I,UAAU7K;AACfmC,aAAK2I,gBAAgB;MACvB;IACF;EACF;AACA,MAAIW,eAAe;AACnB,QAAMtE,SAASkE,SAAS3K,KAAKC,IAAI,GAAG6D,QAAQkH,eAAe,CAAA,GAAI,OAAOvJ,SAAAA;AACpE,UAAMqJ,MAAMrG,SAASvF,OAAOuC,IAAAA;AAC5B,UAAMwJ,cAAc;SAAIxJ,KAAKG,SAASzC,IAAI,CAAC+L,UAAUA,MAAMxL,KAAK;SAAO+B,KAAKwC,YAAY,CAAA;MACrF9E,IAAI,CAACO,UAAU,KAAKA,KAAAA,EAAO,EAC3B8E,KAAK,IAAA;AACR,UAAM2G,UAAUpG,UAAU+F,KAAKjD,iBAAAA;AAC/B,UAAM5B,SAAS,6BAA6BpH,MAAMa,QAAQ,IAAIb,MAAMa,KAAK,MAAM,YAAA;;iBAElE+B,KAAK/B,KAAK;EACzBuL,cAAc;EAAiBA,WAAAA;IAAkB,EAAA;;EAEjDE,OAAAA;;;AAGE,QAAI,CAACJ,gBAAgBrC,OAAO0C,KAAKzM,iBAAiBsH,QAAQ,GAAA,GAAM;AAC9D,UAAI;AACF,cAAMC,QAAQ,MAAMwC,OAAOtC,KACzB;UAAEhG,MAAM;UAAaV,OAAO+B,KAAK/B;UAAOJ,MAAM6L;QAAQ,GACtDxM,gBACAsH,QACA,GAAA;AAEF,cAAMoF,QAAOlF,eAAeD,KAAAA;AAC5B,cAAMiE,UAAU,OAAOkB,OAAMlB,YAAY,WAAWkB,MAAKlB,UAAUjE;AACnE,YAAIiE,QAAQxK,KAAI,GAAI;AAClB8B,eAAK0I,UAAU9E,SAAS8E,SAAS,GAAA;AACjC1I,eAAK2I,gBAAgB;AACrB;QACF;MACF,SAAShB,OAAO;AACd,YAAIA,iBAAiBC,yBAAyB;AAC5C0B,yBAAe;QACjB;AACAjC,2BAAmB,mBAAoBM,MAAgBE,OAAO;MAChE;IACF,OAAO;AACLyB,qBAAe;IACjB;AACA,UAAMzL,OAAO+F,SAASyF,IAAItG,KAAK,MAAA,GAAS,GAAA;AACxC,QAAIlF,MAAM;AACRmC,WAAK0I,UAAU7K;AACfmC,WAAK2I,gBAAgB;IACvB;EACF,CAAA;AACA,MAAIW,cAAc;AAChBjC,uBAAmB;EACrB;AAEA,MAAIwC;AACJ,QAAMC,wBAAwB7H,MAAMhB,MAAM,GAAG,EAAA,EAAIvD,IAAI,CAACsC,UAAU;IAC9D/B,OAAO+B,KAAK/B;IACZ,GAAI+B,KAAK0I,YAAYpL,SAAY;MAAEoL,SAAS1I,KAAK0I;IAAQ,IAAI,CAAC;EAChE,EAAA;AACA,MAAI3C,YAAYF,cAAc5D,MAAM/C,SAAS,GAAG;AAC9C,UAAMsF,SAAS,wEAAwEpH,MAAMa,QAAQ,IAAIb,MAAMa,KAAK,MAAM,EAAA;;EAE5H6L,sBAAsBpM,IAAI,CAACoB,UAAU,KAAKA,MAAMb,KAAK,GAAGa,MAAM4J,UAAU,KAAK5J,MAAM4J,OAAO,KAAK,EAAA,EAAI,EAAE3F,KAAK,IAAA,CAAA;;;AAGxG,QAAIkE,OAAO0C,KAAKzM,iBAAiBsH,QAAQ,GAAA,GAAM;AAC7C,UAAI;AACFqF,sBAAcjG,SACZ,MAAMqD,OAAOtC,KACX;UACEhG,MAAM;UACN,GAAIvB,MAAMa,UAAUX,SAAY;YAAEW,OAAOb,MAAMa;UAAM,IAAI,CAAC;UAC1Dc,SAAS+K;QACX,GACA5M,gBACAsH,QACA,GAAA,GAEF,GAAA;MAEJ,SAASmD,OAAO;AACdN,2BAAmB,uBAAwBM,MAAgBE,OAAO;MACpE;IACF;EACF;AACA,MACEgC,gBAAgBvM,UAChB+E,QAAQO,UAAUiH,gBAAgBvM,UAClC2L,kBAAkB,GAClB;AACAY,kBAAcxH,QAAQO,SAASiH;EACjC;AAEA,QAAM9C,OAAqB;IACzBgD,YAAY1H,QAAQ0H;IACpBC,SAAS;IACT,GAAI5M,MAAMa,UAAUX,SAAY;MAAEW,OAAOb,MAAMa;IAAM,IAAI,CAAC;IAC1D,GAAI4L,cAAc;MAAEA;IAAY,IAAI,CAAC;IACrCvC;IACAf;IACA9G,WAAWhC,MAAMyB;IACjB+C;IACAgI,OAAO;MACLC,UAAUjD,OAAOkD;MACjBC,aAAanD,OAAOmD;MACpBC,cAAcpD,OAAOoD;MACrBC,YAAY5E,KAAKC,IAAG,IAAKF;MACzB,GAAI4B,mBAAmB/J,SAAY;QAAE+J;MAAe,IAAI,CAAC;MACzDkD,iBAAiBtD,OAAOuD;MACxBvB;IACF;IACA,GAAI5G,QAAQsE,aAAarJ,SAAY;MAAEqJ,UAAUtE,QAAQsE;IAAS,IAAI,CAAC;IACvE,GAAItE,QAAQwE,WAAWvJ,SAAY;MAAEuJ,QAAQxE,QAAQwE;IAAO,IAAI,CAAC;IACjE4D,UAAS,oBAAI/E,KAAAA,GAAOgF,YAAW;EACjC;AACA,SAAO;IAAE3D;IAAMtJ;IAAOuJ,WAAW;EAAM;AACzC;AA9SsBxB;;;ACjiBtB,IAAMmF,OAAO,IAAIC,IACf,sRAAsRC,MACpR,GAAA,CAAA;AAIJ,SAASC,MAAMC,MAAY;AACzB,SAAO,IAAIH,KAEPG,KAAKC,YAAW,EAAGC,MAAM,6DAAA,KAAkE,CAAA,GAC3FC,OAAO,CAACC,SAASA,KAAKC,UAAU,KAAK,CAACT,KAAKU,IAAIF,IAAAA,CAAAA,CAAAA;AAErD;AANSL;AAQT,SAASQ,QAAQC,OAAoBR,MAAY;AAC/C,MAAIS,QAAQ;AACZ,aAAWL,QAAQL,MAAMC,IAAAA,GAAO;AAC9B,QAAIQ,MAAMF,IAAIF,IAAAA,GAAO;AACnBK,eAAS;IACX;EACF;AACA,SAAOA;AACT;AARSF;AAsBF,SAASG,iBAAAA;AACd,SAAO,OAAOC,YAAAA;AACZ,UAAM,EAAEC,KAAI,IAAKD;AACjB,QAAIX;AACJ,YAAQY,KAAKC,MAAI;MACf,KAAK,YAAY;AACf,cAAML,QAAQT,MAAMa,KAAKJ,KAAK;AAC9B,cAAMM,SAASF,KAAKG,WACjBC,IAAI,CAACC,YAAWC,WAAW;UAC1BD,WAAAA;UACAC;UACAT,OAAOF,QAAQC,OAAO,GAAGS,WAAUE,KAAK,IAAIF,WAAUG,WAAW,EAAA,EAAI;QACvE,EAAA,EACCjB,OAAO,CAACkB,UAAUA,MAAMZ,QAAQ,CAAA,EAChCa,KACC,CAACC,GAAGC,MACFA,EAAEf,QAAQc,EAAEd,UACXe,EAAEP,UAAUQ,SAAS,MAAMF,EAAEN,UAAUQ,SAAS,MACjDF,EAAEL,QAAQM,EAAEN,KAAK,EAEpBQ,MAAM,GAAGd,KAAKe,QAAQ,EACtBX,IAAI,CAACK,UAAUA,MAAMJ,SAAS;AACjCjB,eACEY,KAAKgB,SAAS,gBACVC,KAAKC,UAAU;UAAEC,UAAU;UAAmBC,MAAMlB,OAAOE,IAAI,CAACiB,MAAMA,EAAEC,EAAE;QAAE,CAAA,IAC5EL,KAAKC,UAAU;UACbC,UAAU;UACVI,QAAQrB,OAAOX,OAAO,CAAC8B,MAAMA,EAAEG,WAAW,EAAEpB,IAAI,CAACiB,MAAMA,EAAEC,EAAE;UAC3DF,MAAMlB,OAAOX,OAAO,CAAC8B,MAAM,CAACA,EAAEG,WAAW,EAAEpB,IAAI,CAACiB,MAAMA,EAAEC,EAAE;QAC5D,CAAA;AACN;MACF;MACA,KAAK,UAAU;AACb,cAAM1B,QAAQT,MAAMa,KAAKJ,KAAK;AAC9B,cAAMM,SAASF,KAAKyB,UACjBrB,IAAI,CAACsB,cAAc;UAClBJ,IAAII,SAASJ;UACbzB,OAAOF,QAAQC,OAAO,GAAG8B,SAASnB,SAAS,EAAA,IAAMmB,SAASC,eAAe,EAAA,EAAI;QAC/E,EAAA,EACCpC,OAAO,CAACkB,UAAUA,MAAMZ,QAAQ,CAAA,EAChCa,KAAK,CAACC,GAAGC,MAAMA,EAAEf,QAAQc,EAAEd,KAAK,EAChCiB,MAAM,GAAGd,KAAK4B,YAAY,EAC1BxB,IAAI,CAACK,UAAUA,MAAMa,EAAE;AAC1BlC,eAAO6B,KAAKC,UAAU;UAAEC,UAAU;UAAmBM,WAAWvB;QAAO,CAAA;AACvE;MACF;MACA,KAAK,aAAa;AAChB,cAAM2B,OAAO7B,KAAKZ,KAAK0C,QAAQ,QAAQ,GAAA,EAAKC,KAAI;AAChD,cAAMC,WAAW,kBAAkBC,KAAKJ,IAAAA,IAAQ,CAAA,KAAMA;AACtDzC,eAAO,GAAGY,KAAKO,KAAK,KAAKyB,SAASlB,MAAM,GAAG,GAAA,EAAKiB,KAAI,CAAA;AACpD;MACF;MACA,KAAK;AACH3C,eAAO,GAAGY,KAAKO,SAAS,UAAA,aAAuBP,KAAKkC,QAAQ9B,IAAI,CAACK,UAAUA,MAAMF,KAAK,EAAE4B,KAAK,IAAA,CAAA;AAC7F;MACF,KAAK,aAAa;AAChB,cAAMC,WAA6D,CAAA;AACnE,mBAAWC,QAAQrC,KAAKsC,OAAO;AAC7B,qBAAWC,QAAQF,KAAKjD,KAAKF,MAAM,IAAA,GAAO;AACxC,kBAAMsD,UAAUD,KAAKR,KAAI;AACzB,kBAAMU,WAAW,oBAAoBR,KAAKO,OAAAA;AAC1C,gBAAIC,WAAW,CAAA,MAAOC,UAAaD,SAAS,CAAA,MAAOC,QAAW;AAC5DN,uBAASO,KAAK;gBAAEpC,OAAOkC,SAAS,CAAA;gBAAIG,OAAOH,SAAS,CAAA,EAAGhD;gBAAQ4C,MAAMA,KAAKA;cAAK,CAAA;YACjF,WAAW,8BAA8BQ,KAAKL,OAAAA,KAAY,WAAWK,KAAKL,OAAAA,GAAU;AAClFJ,uBAASO,KAAK;gBAAEpC,OAAOiC;gBAASI,OAAO;gBAAGP,MAAMA,KAAKA;cAAK,CAAA;YAC5D;UACF;QACF;AACAjD,eAAO6B,KAAKC,UAAU;UAAEkB;QAAS,CAAA;AACjC;MACF;IACF;AACA,WAAO;MACLhD;MACA0D,OAAO;QACLC,aAAaC,KAAKC,KAAKlD,QAAQmD,OAAOzD,SAAS,CAAA;QAC/C0D,cAAcH,KAAKC,KAAK7D,KAAKK,SAAS,CAAA;MACxC;IACF;EACF;AACF;AAhFgBK;;;ACUT,SAASsD,SAASC,MAAkB;AACzC,SAAO;IACLC,YAAYD,KAAKC;IACjB,GAAID,KAAKE,UAAUC,SAAY;MAAED,OAAOF,KAAKE;IAAM,IAAI,CAAC;IACxD,GAAIF,KAAKI,gBAAgBD,SAAY;MAAEC,aAAaJ,KAAKI;IAAY,IAAI,CAAC;IAC1EC,WAAWL,KAAKK;IAChBC,aAAaN,KAAKM;IAClBC,WAAWP,KAAKO;IAChB,GAAIP,KAAKQ,aAAaL,SAAY;MAAEK,UAAUR,KAAKQ;IAAS,IAAI,CAAC;IACjE,GAAIR,KAAKS,WAAWN,SAAY;MAAEM,QAAQT,KAAKS;IAAO,IAAI,CAAC;IAC3DC,SAASV,KAAKU;EAChB;AACF;AAZgBX;AAchB,SAASY,OAAOX,MAAoBY,QAA2C;AAC7E,MAAIA,WAAWT,UAAaU,OAAOC,KAAKF,MAAAA,EAAQG,WAAW,GAAG;AAC5D,WAAO;EACT;AACA,MAAIC,qBAAqBJ,MAAAA,GAAS;AAChC,WAAO;EACT;AACA,SAAOK,cAAcjB,KAAKQ,UAAUI,MAAAA;AACtC;AARSD;AAWF,IAAMO,0BAAN,MAAMA;EAzEb,OAyEaA;;;EACMC,QAAQ,oBAAIC,IAAAA;EAE7B,MAAMC,IAAIrB,MAAoBsB,OAAkC;AAC9D,SAAKH,MAAMI,IAAIvB,KAAKC,YAAY;MAC9BD,MAAMwB,gBAAgBxB,IAAAA;MACtBsB,OAAOA,MAAMG,IAAI,CAACC,UAAU;QAAE,GAAGA;MAAK,EAAA;IACxC,CAAA;EACF;EAEA,MAAMC,IACJ1B,YACAW,QACmC;AACnC,UAAMgB,QAAQ,KAAKT,MAAMQ,IAAI1B,UAAAA;AAC7B,WAAO2B,UAAUzB,UAAaQ,OAAOiB,MAAM5B,MAAMY,MAAAA,IAC7CY,gBAAgBI,MAAM5B,IAAI,IAC1BG;EACN;EAEA,MAAM0B,QACJC,aACAlB,QACyB;AACzB,UAAMO,QAAwB,CAAA;AAC9B,eAAWY,MAAM,IAAIC,IAAIF,WAAAA,GAAc;AACrC,YAAM9B,OAAO,MAAM,KAAK2B,IAAII,IAAInB,MAAAA;AAChC,UAAIZ,SAASG,QAAW;AACtBgB,cAAMc,KAAKjC,IAAAA;MACb;IACF;AACA,WAAOmB;EACT;EAEA,MAAMe,KACJC,UAII,CAAC,GAC0B;AAC/B,UAAMC,MAAMD,QAAQL,gBAAgB3B,SAAY,IAAI6B,IAAIG,QAAQL,WAAW,IAAI3B;AAC/E,UAAMkC,UAAU;SAAI,KAAKlB,MAAMmB,OAAM;MAClC1B,OACC,CAAC,EAAEZ,KAAI,OACJoC,QAAQjC,UAAaiC,IAAIG,IAAIvC,KAAKC,UAAU,MAAMU,OAAOX,MAAMmC,QAAQvB,MAAM,CAAA,EAEjFa,IAAI,CAAC,EAAEzB,KAAI,MAAOD,SAASC,IAAAA,CAAAA,EAC3BwC,KAAK,CAACC,GAAGC,MAAMD,EAAExC,WAAW0C,cAAcD,EAAEzC,UAAU,CAAA;AACzD,WAAOkC,QAAQS,UAAUzC,SAAYkC,QAAQQ,MAAM,GAAGV,QAAQS,KAAK,IAAIP;EACzE;EAEA,MAAMS,UAAU7C,YAAoB8C,OAAeC,KAAkC;AACnF,UAAM1B,QAAQ,KAAKH,MAAMQ,IAAI1B,UAAAA,GAAaqB,SAAS,CAAA;AACnD,WAAOA,MAAMuB,MAAMI,KAAKC,IAAI,GAAGH,KAAAA,GAAQE,KAAKC,IAAI,GAAGF,MAAM,CAAA,CAAA,EAAIvB,IAAI,CAACC,UAAU;MAAE,GAAGA;IAAK,EAAA;EACxF;EAEA,MAAMyB,OAAOlD,YAAmC;AAC9C,SAAKkB,MAAMiC,OAAOnD,UAAAA;EACpB;AACF;;;AC1GA,IAAMoD,aAAa;AAEnB,SAASC,KAAQC,OAAoC;AACnD,MAAIA,UAAU,QAAQA,UAAUC,QAAW;AACzC,WAAOA;EACT;AACA,SAAO,OAAOD,UAAU,WAAYE,KAAKC,MAAMH,KAAAA,IAAeA;AAChE;AALSD;AAoBF,IAAMK,sBAAN,MAAMA;EAjDb,OAiDaA;;;EACQC;EACAC;EACAC;EACFC;EAEjB,YAAYH,QAAwBI,UAAsC,CAAC,GAAG;AAC5E,SAAKJ,SAASK,WAAWL,MAAAA;AACzB,SAAKC,QAAQG,QAAQH,SAAS;AAC9B,SAAKC,aAAaE,QAAQF,cAAc,GAAG,KAAKD,KAAK;AACrD,eAAWK,QAAQ;MAAC,KAAKL;MAAO,KAAKC;OAAa;AAChD,UAAI,CAACT,WAAWc,KAAKD,IAAAA,GAAO;AAC1B,cAAM,IAAIE,MAAM,2CAA2CX,KAAKY,UAAUH,IAAAA,CAAAA,EAAO;MACnF;IACF;AACA,SAAKH,kBAAkBO,KAAKC,IAAI,GAAGD,KAAKE,MAAMR,QAAQD,mBAAmB,GAAA,CAAA;EAC3E;;EAGAU,mBAA6B;AAC3B,UAAMC,QAAQ,KAAKb,MAAMc,QAAQ,KAAK,GAAA;AACtC,WAAO;MACL,8BAA8B,KAAKd,KAAK;;;;;;;;;;;;MAYxC,8BAA8Ba,KAAAA,oBAAyB,KAAKb,KAAK;MACjE,8BAA8B,KAAKC,UAAU;;;;;;;;EAQjD;EAEA,MAAMc,eAA8B;AAClC,eAAWC,aAAa,KAAKJ,iBAAgB,GAAI;AAC/C,YAAM,KAAKb,OAAOkB,MAAMD,SAAAA;IAC1B;EACF;;;;;EAMUE,gBACRC,QACAC,QACU;AACV,WAAOC,WAAWF,QAAQC,MAAAA;EAC5B;;EAGA,MAAME,IAAIC,MAAoBC,OAAkC;AAC9D,UAAMC,SAAQC,cAAcH,IAAAA;AAC5B,UAAM,EAAEI,UAAU,GAAGC,KAAAA,IAASH;AAC9B,UAAMI,QAAQ,8BAAO9B,WAAAA;AACnB,YAAMA,OAAOkB,MAAM,eAAe,KAAKhB,UAAU,2BAA2B;QAC1EwB,OAAMK;OACP;AACD,eAASC,QAAQ,GAAGA,QAAQP,MAAMQ,QAAQD,SAAS,KAAK7B,iBAAiB;AACvE,cAAM+B,QAAQT,MAAMU,MAAMH,OAAOA,QAAQ,KAAK7B,eAAe;AAC7D,cAAMkB,SAAoB,CAAA;AAC1B,cAAMe,OAAOF,MAAMG,IAAI,CAACC,SAAAA;AACtBjB,iBAAOkB,KAAKb,OAAMK,YAAYO,KAAKxB,OAAOwB,KAAKE,QAAQ,MAAMb,cAAcW,KAAKG,IAAI,CAAA;AACpF,gBAAMC,KAAKrB,OAAOY;AAClB,iBAAO,KAAKS,KAAK,CAAA,MAAOA,KAAK,CAAA,MAAOA,KAAK,CAAA,MAAOA,EAAAA;QAClD,CAAA;AACA,cAAM1C,OAAOkB,MACX,eAAe,KAAKhB,UAAU,0CAA0CkC,KAAKO,KAAK,IAAA,CAAA,IAClFtB,MAAAA;MAEJ;AACA,YAAMrB,OAAOkB,MACX,eAAe,KAAKjB,KAAK;;;;;;8FAOzB;QACEyB,OAAMK;QACNL,OAAMkB,SAAS;QACflB,OAAMmB,eAAe;QACrBnB,OAAMoB;QACNpB,OAAMqB;QACNrB,OAAMsB;QACNtB,OAAMuB,UAAU;QAChBrB,aAAahC,SAAY,OAAOC,KAAKY,UAAUmB,QAAAA;QAC/C/B,KAAKY,UAAUoB,IAAAA;QACfH,OAAMwB;OACP;IAEL,GAtCc;AAuCd,UAAMC,cAAc,KAAKnD,OAAOmD,aAAaC,KAAK,KAAKpD,MAAM;AAC7D,WAAOmD,gBAAgBvD,SAAYuD,YAAYrB,KAAAA,IAASA,MAAM,KAAK9B,MAAM;EAC3E;EAEA,MAAMqD,IACJtB,YACAX,QACmC;AACnC,YAAQ,MAAM,KAAKkC,QAAQ;MAACvB;OAAaX,MAAAA,GAAS,CAAA;EACpD;EAEA,MAAMkC,QACJC,aACAnC,QACyB;AACzB,QAAImC,YAAYtB,WAAW,GAAG;AAC5B,aAAO,CAAA;IACT;AACA,UAAMZ,SAAoB;MAAC;WAAI,IAAImC,IAAID,WAAAA;;;AACvC,UAAME,aAAa;MAAC;SAAoC,KAAKtC,gBAAgBC,QAAQC,MAAAA;;AACrF,UAAMe,OAAO,MAAM,KAAKpC,OAAOkB,MAC7B,2CAA2C,KAAKjB,KAAK,UAAUwD,WAAWd,KAAK,OAAA,CAAA,IAC/EtB,MAAAA;AAEF,WAAOe,KAAKC,IAAI,CAACqB,QAAAA;AACf,YAAMlC,OAAO9B,KAAqCgE,IAAIlC,IAAI;AAC1D,YAAMI,WAAWlC,KAA8BgE,IAAI9B,QAAQ;AAC3D,aAAO;QAAE,GAAGJ;QAAM,GAAII,aAAahC,SAAY;UAAEgC;QAAS,IAAI,CAAC;MAAG;IACpE,CAAA;EACF;EAEA,MAAM+B,KACJvD,UAII,CAAC,GAC0B;AAC/B,UAAMiB,SAAoB,CAAA;AAC1B,UAAMoC,aAAa,KAAKtC,gBAAgBf,QAAQgB,QAAQC,MAAAA;AACxD,QAAIjB,QAAQmD,gBAAgB3D,QAAW;AACrCyB,aAAOkB,KAAK;WAAInC,QAAQmD;OAAY;AACpCE,iBAAWlB,KAAK,sBAAsBlB,OAAOY,MAAM,WAAW;IAChE;AACA,QAAI2B,QAAQ;AACZ,QAAIxD,QAAQwD,UAAUhE,QAAW;AAC/ByB,aAAOkB,KAAK7B,KAAKC,IAAI,GAAGD,KAAKE,MAAMR,QAAQwD,KAAK,CAAA,CAAA;AAChDA,cAAQ,UAAUvC,OAAOY,MAAM;IACjC;AACA,UAAMG,OAAO,MAAM,KAAKpC,OAAOkB,MAC7B;gBACU,KAAKjB,KAAK;WACfwD,WAAWxB,SAAS,IAAI,SAASwB,WAAWd,KAAK,OAAA,CAAA,KAAa,EAAA;gCACzCiB,KAAAA,IAC1BvC,MAAAA;AAEF,WAAOe,KAAKC,IAAI,CAACqB,QAAAA;AACf,YAAM9B,WAAWlC,KAA8BgE,IAAI9B,QAAQ;AAC3D,aAAOiC,SAAS;QACd9B,YAAY2B,IAAII;QAChBC,SAAS;QACT,GAAIL,IAAId,UAAU,OAAO;UAAEA,OAAOc,IAAId;QAAM,IAAI,CAAC;QACjD,GAAIc,IAAIb,gBAAgB,OAAO;UAAEA,aAAaa,IAAIb;QAAY,IAAI,CAAC;QACnEC,WAAWY,IAAIZ;QACfC,aAAaW,IAAIX;QACjBC,WAAWgB,OAAON,IAAIO,UAAU;QAChCC,OAAO,CAAA;QACPC,OAAO;UACLC,UAAU;UACVC,aAAa;UACbC,cAAc;UACdC,YAAY;UACZC,iBAAiB;UACjBC,iBAAiB;QACnB;QACA,GAAI7C,aAAahC,SAAY;UAAEgC;QAAS,IAAI,CAAC;QAC7C,GAAI8B,IAAIT,WAAW,OAAO;UAAEA,QAAQS,IAAIT;QAAO,IAAI,CAAC;QACpDC,SAASQ,IAAIgB,oBAAoBC,OAAOjB,IAAIgB,SAASE,YAAW,IAAKC,OAAOnB,IAAIgB,QAAQ;MAC1F,CAAA;IACF,CAAA;EACF;EAEA,MAAMI,UAAU/C,YAAoBC,OAAe+C,KAAkC;AACnF,UAAM3C,OAAO,MAAM,KAAKpC,OAAOkB,MAC7B,+BAA+B,KAAKhB,UAAU;wEAE9C;MAAC6B;MAAYrB,KAAKC,IAAI,GAAGqB,KAAAA;MAAQ+C;KAAI;AAEvC,WAAO3C,KAAKC,IAAI,CAACqB,SAAS;MACxB5C,OAAOkD,OAAON,IAAIsB,GAAG;MACrBvC,MAAMiB,IAAIjB;MACV,GAAIiB,IAAIlB,SAAS,OAAO;QAAEA,MAAMwB,OAAON,IAAIlB,IAAI;MAAE,IAAI,CAAC;IACxD,EAAA;EACF;EAEA,MAAMyC,OAAOlD,YAAmC;AAC9C,UAAMmD,OAAO,8BAAOlF,WAAAA;AAClB,YAAMA,OAAOkB,MAAM,eAAe,KAAKhB,UAAU,2BAA2B;QAAC6B;OAAW;AACxF,YAAM/B,OAAOkB,MAAM,eAAe,KAAKjB,KAAK,2BAA2B;QAAC8B;OAAW;IACrF,GAHa;AAIb,UAAMoB,cAAc,KAAKnD,OAAOmD,aAAaC,KAAK,KAAKpD,MAAM;AAC7D,WAAOmD,gBAAgBvD,SAAYuD,YAAY+B,IAAAA,IAAQA,KAAK,KAAKlF,MAAM;EACzE;AACF;;;ACrOA,eAAsBmF,kBACpBC,OACAC,SAAiC;AAEjC,QAAM,EAAEC,OAAOC,WAAW,IAAIC,WAAW,KAAQ,GAAGC,MAAAA,IAAUJ;AAC9D,QAAM,EAAEK,MAAK,IAAKC,QAAQP,KAAAA;AAC1B,QAAMQ,QAAQF,MAAMG,OAAO,CAACC,OAAOC,SAASD,QAAQC,KAAKC,KAAKC,QAAQ,CAAA;AACtE,MAAIP,MAAMO,SAASV,YAAYK,QAAQJ,UAAU;AAC/C,UAAMF,MAAMY,OAAOb,QAAQc,UAAU;AACrC,WAAO;MACLC,QAAQ;MACRC,QAAQ,GAAGX,MAAMO,MAAM,YAAYL,KAAAA,kCAAuCL,QAAAA,aAAqBC,QAAAA;IACjG;EACF;AACA,QAAMc,WAAW,MAAMhB,MAAMiB,IAAIlB,QAAQc,UAAU;AACnD,QAAMK,QAAQ,MAAMC,kBAAkBrB,OAAO;IAC3C,GAAGK;IACH,GAAIa,aAAaI,SAAY;MAAEJ;IAAS,IAAI,CAAC;EAC/C,CAAA;AACA,MAAIE,MAAMG,WAAW;AACnB,UAAMC,gBACJC,KAAKC,UAAUR,UAAUS,YAAY,IAAA,MAAUF,KAAKC,UAAUN,MAAMQ,KAAKD,YAAY,IAAA,KACrFT,UAAUW,WAAWT,MAAMQ,KAAKC;AAClC,QAAIL,eAAe;AACjB,YAAMtB,MAAM4B,IAAIV,MAAMQ,MAAMR,MAAMd,KAAK;IACzC;AACA,WAAO;MAAEU,QAAQ;MAAaY,MAAMR,MAAMQ;IAAK;EACjD;AACA,QAAM1B,MAAM4B,IAAIV,MAAMQ,MAAMR,MAAMd,KAAK;AACvC,SAAO;IAAEU,QAAQ;IAASY,MAAMR,MAAMQ;EAAK;AAC7C;AA9BsB7B;;;ACkFtB,IAAMgC,kBACJ;AAEF,IAAMC,kBAAiB;EACrBC,UAAU;EACVC,gBAAgB;EAChBC,iBAAiB;EACjBC,WAAW;AACb;AAEA,SAASC,MAAMC,MAAqD;AAClE,MAAIA,KAAKC,cAAcC,QAAW;AAChC,WAAOA;EACT;AACA,SAAOF,KAAKG,YAAYD,UAAaF,KAAKG,YAAYH,KAAKC,YACvD,MAAMD,KAAKC,SAAS,KACpB,OAAOD,KAAKC,SAAS,IAAID,KAAKG,OAAO;AAC3C;AAPSJ;AAST,SAASK,UAAUJ,MAAwBK,cAAsBC,OAAc;AAC7E,QAAMC,aAAaR,MAAMC,IAAAA;AACzB,MAAIQ,UAAUR,KAAKQ;AACnB,MAAIA,YAAYN,UAAaM,QAAQC,SAASJ,cAAc;AAC1DG,cAAU,GAAGA,QAAQE,MAAM,GAAGC,KAAKC,IAAI,GAAGP,eAAe,CAAA,CAAA,CAAA;EAC3D;AACA,MAAIL,KAAKa,aAAaX,UAAaF,KAAKa,SAASJ,SAAS,KAAKJ,eAAe,GAAG;AAC/EG,cAAU,GAAGA,WAAW,EAAA,GAAKA,UAAU,MAAM,EAAA,aAAeR,KAAKa,SAASH,MAAM,GAAG,EAAA,EAAII,KAAK,IAAA,CAAA;EAC9F;AACA,SAAO;IACLC,IAAIf,KAAKe;IACTC,OAAOhB,KAAKgB;IACZ,GAAIR,WAAWH,eAAe,IAAI;MAAEG;IAAQ,IAAI,CAAC;IACjD,GAAID,eAAeL,SAAY;MAAEH,OAAOQ;IAAW,IAAI,CAAC;IACxD,GAAID,UAAUJ,SAAY;MAAEI;IAAM,IAAI;MAAEW,aAAajB,KAAKkB,SAAST,SAAS;IAAE;EAChF;AACF;AAhBSL;AAkBT,SAASe,OAAOC,YAA8B;AAC5C,SAAOA,WACJC,IAAI,CAACC,UAAAA;AACJ,UAAMC,SAAS,KAAKC,OAAOF,MAAMhB,SAAS,CAAA;AAC1C,UAAMmB,OAAO;MAACH,MAAMvB;MAAOuB,MAAML,cAAc,oBAAoBf;MAChEwB,OAAOC,OAAAA,EACPb,KAAK,IAAA;AACR,WAAO,GAAGS,MAAAA,IAAUD,MAAMP,EAAE,KAAKO,MAAMN,KAAK,GAAGS,OAAO,KAAKA,IAAAA,MAAU,EAAA,GAAKH,MAAMd,UAAU,WAAMc,MAAMd,OAAO,KAAK,EAAA;EACpH,CAAA,EACCM,KAAK,IAAA;AACV;AAVSK;AAaT,SAASS,IACPC,OACAC,UAAgB;AAEhB,MAAIC;AACJ,aAAW1B,gBAAgB;IAAC;IAAK;IAAK;KAAI;AACxC,UAAMe,aAAaS,MAAMR,IAAI,CAAC,EAAErB,MAAMM,MAAK,MAAOF,UAAUJ,MAAMK,cAAcC,KAAAA,CAAAA;AAChF,UAAM0B,OAAOb,OAAOC,UAAAA;AACpBW,WAAO;MAAEX;MAAYY;IAAK;AAC1B,QAAIA,KAAKvB,UAAUqB,UAAU;AAC3B;IACF;EACF;AACA,SAAOC;AACT;AAdSH;AAoCF,IAAMK,0BAAN,MAAMA;EAlMb,OAkMaA;;;;EACFC;EACQC;EAEjB,YAA6BC,SAAyC;SAAzCA,UAAAA;AAC3B,SAAKF,QAAQE,QAAQF;AACrB,SAAKC,MAAMC,QAAQD;EACrB;EAEAE,oBAAyC;AACvC,WAAO;MAAEC,WAAW;IAAkB;EACxC;EAEA,MAAMC,SAASC,OAAeJ,UAA2B,CAAC,GAAuB;AAC/E,UAAMK,SAAS,MAAM,KAAKC,OACxBF,OACAJ,QAAQV,WAAWxB,SAAY;MAAEwB,QAAQU,QAAQV;IAAO,IAAI,CAAC,CAAA;AAE/D,WAAOe,OAAOE,SAASjC,MAAM,GAAG0B,QAAQQ,QAAQH,OAAOE,SAASlC,MAAM;EACxE;;EAGA,MAAMiC,OAAOF,OAAeJ,UAA2B,CAAC,GAA8B;AACpF,UAAMS,eAAelC,KAAKC,IAAI,GAAG,KAAKwB,QAAQS,gBAAgB,CAAA;AAC9D,UAAMC,gBAAgBnC,KAAKC,IAAIiC,cAAc,KAAKT,QAAQW,0BAA0B,GAAA;AACpF,UAAMC,UAAU,MAAM,KAAKd,MAAMe,KAAK;MACpC,GAAIb,QAAQV,WAAWxB,SAAY;QAAEwB,QAAQU,QAAQV;MAAO,IAAI,CAAC;MACjEwB,OAAOJ;IACT,CAAA;AACA,QAAIE,QAAQvC,UAAUoC,cAAc;AAClC,aAAO,KAAKM,SACVX,OACAQ,QAAQ3B,IAAI,CAAC+B,WAAWA,OAAOC,UAAU,GACzCjB,OAAAA;IAEJ;AACA,UAAMkB,SAAS,IAAIC,YACjB,KAAKpB,KACL;MAAE,GAAGzC;MAAgB,GAAG,KAAK0C,QAAQkB;MAAQ,GAAGlB,QAAQkB;IAAO,GAC/DlB,QAAQoB,MAAM;AAEhB,UAAMC,YAAYT,QAAQ3B,IAAI,CAAC+B,YAAY;MACzCrC,IAAIqC,OAAOC;MACX,GAAID,OAAOpC,UAAUd,SAAY;QAAEc,OAAOoC,OAAOpC;MAAM,IAAI,CAAC;MAC5D,GAAIoC,OAAOM,gBAAgBxD,SACvB;QAAEwD,aAAaN,OAAOM,YAAYhD,MAAM,GAAG,GAAA;MAAK,IAChD,CAAC;IACP,EAAA;AACA,UAAMiD,SAAS,kGAAkGd,YAAAA;;YAEzGL,KAAAA;;;EAGViB,UAAUpC,IAAI,CAACuC,aAAa,IAAIA,SAAS7C,EAAE,KAAK6C,SAAS5C,SAAS,EAAA,GAAK4C,SAASF,cAAc,WAAME,SAASF,WAAW,KAAK,EAAA,EAAI,EAAE5C,KAAK,IAAA,CAAA;;;AAGtI,UAAM+C,QAAQ,IAAIC,IAAIL,UAAUpC,IAAI,CAACuC,aAAaA,SAAS7C,EAAE,CAAA;AAC7D,QAAI;AACF,YAAMgD,QAAQC,eACZ,MAAMV,OAAOW,KACX;QAAEC,MAAM;QAAU1B;QAAOiB;QAAWZ;MAAa,GACjDpD,iBACAkE,QACA,GAAA,CAAA;AAGJ,YAAMQ,WAAW;WACZ,IAAIL,IAAIM,WAAWL,OAAON,aAAaM,OAAOM,MAAAA,EAAQ3C,OAAO,CAACX,OAAO8C,MAAMS,IAAIvD,EAAAA,CAAAA,CAAAA;QAClFL,MAAM,GAAGmC,YAAAA;AACX,YAAMJ,SAAS,MAAM,KAAKU,SAASX,OAAO2B,UAAU/B,OAAAA;AACpD,YAAMmC,YACJ,OAAOR,OAAOS,aAAa,WAAWT,MAAMS,SAAS9D,MAAM,GAAG,GAAA,IAASR;AACzE,aAAO;QACL,GAAGuC;QACHgC,WAAW;UACTrD,YAAYqC,UAAUhD;UACtB0D;UACA,GAAII,cAAcrE,SAAY;YAAEqE;UAAU,IAAI,CAAC;QACjD;MACF;IACF,SAASG,OAAO;AACd,aAAO;QACL/B,UAAU,CAAA;QACVc,WAAW,CAAA;QACXgB,WAAW;UAAErD,YAAYqC,UAAUhD;UAAQ0D,UAAU,CAAA;UAAIO,OAAQA,MAAgBC;QAAQ;MAC3F;IACF;EACF;;;;;;EAOA,MAAMxB,SACJX,OACAoC,aACAxC,UAA2B,CAAC,GACD;AAC3B,UAAMyC,MAAM;SAAI,IAAIf,IAAIc,WAAAA;;AACxB,UAAME,QAAQ,IAAIC,KACf,MAAM,KAAK7C,MAAM8C,QAAQH,KAAKzC,QAAQV,MAAM,GAAGL,IAAI,CAAC4D,SAAS;MAACA,KAAK5B;MAAY4B;KAAK,CAAA;AAEvF,UAAMC,cAAc,MAAMC,SACxBN,KACAlE,KAAKC,IAAI,GAAG,KAAKwB,QAAQgD,eAAe,CAAA,GACxC,OAAOrE,OAAAA;AACL,YAAMkE,OAAOH,MAAMO,IAAItE,EAAAA;AACvB,UAAIkE,SAAS/E,QAAW;AACtB,eAAO;UACLoF,YAAY;YACVjC,YAAYtC;YACZwE,MAAM;YACNC,OAAO,CAAA;YACP3D,OAAO,CAAA;YACP4D,UAAU;YACVC,aAAa;YACbC,cAAc;YACdC,WAAW;UACb;UACAjD,UAAU,CAAA;QACZ;MACF;AACA,YAAM2C,aAAa,MAAM,KAAKO,KAAKrD,OAAOyC,MAAM7C,OAAAA;AAChD,aAAO;QAAEkD;QAAY3C,UAAU,MAAM,KAAKmD,WAAWb,MAAMK,UAAAA;MAAY;IACzE,CAAA;AAGF,UAAMS,kBAAkB,KAAK3D,QAAQ2D,mBAAmB;AACxD,QAAIC,OAAO;AACX,UAAMrD,WAAsB,CAAA;AAC5B,eAAWrB,SAAS4D,aAAa;AAC/B,iBAAWe,WAAW3E,MAAMqB,UAAU;AACpC,YAAIqD,QAAQD,iBAAiB;AAC3B;QACF;AACA,cAAM/D,OAAOiE,QAAQjE,KAAKtB,MAAM,GAAGqF,kBAAkBC,IAAAA;AACrDA,gBAAQhE,KAAKvB;AACbkC,iBAASuD,KAAK;UAAE,GAAGD;UAASjE;QAAK,CAAA;MACnC;IACF;AACA,WAAO;MAAEW;MAAUc,WAAWyB,YAAY7D,IAAI,CAACC,UAAUA,MAAMgE,UAAU;IAAE;EAC7E;EAEA,MAAcO,KACZrD,OACAyC,MACA7C,SAC6B;AAC7B,UAAMkB,SAAS,IAAIC,YACjB,KAAKpB,KACL;MAAE,GAAGzC;MAAgB,GAAG,KAAK0C,QAAQkB;MAAQ,GAAGlB,QAAQkB;IAAO,GAC/DlB,QAAQoB,MAAM;AAEhB,UAAM2C,WAAWxF,KAAKC,IAAI,GAAG,KAAKwB,QAAQ+D,YAAY,CAAA;AACtD,UAAMC,qBAAqB,KAAKhE,QAAQgE,sBAAsB;AAC9D,UAAMC,QAAQC,UAAUrB,IAAAA;AACxB,UAAMsB,QAAQ;SAAIC,SAASvB,KAAKpD,KAAK;MAAGR,IAAI,CAAC,EAAErB,MAAMyG,KAAI,OAAQ;MAAEzG;MAAMM,OAAOmG,KAAKhG;IAAO,EAAA;AAC5F,UAAMiG,UAAU9E,IAAI2E,OAAOI,OAAOC,iBAAiB;AACnD,UAAM5F,QAAQiE,KAAKjE,SAASiE,KAAK4B,UAAU5B,KAAK5B;AAChD,UAAMmC,QAA0B,CAAA;AAChC,UAAMsB,SAAmB,CAAA;AACzB,QAAIlB;AACJ,QAAIlB;AACJ,UAAMa,OACJmB,QAAQ1E,KAAKvB,UAAU2F,qBAAqB,gBAAgB;AAE9D,UAAMW,OAAO,wBAACC,WAAAA;AACZ,UAAIA,kBAAkBC,yBAAyB;AAC7CrB,oBAAYoB,OAAO9D,UAAU,YAAY,YAAY;MACvD,OAAO;AACL0C,oBAAY;MACd;AACAlB,cAASsC,OAAiBrC;IAC5B,GAPa;AASb,QAAIY,SAAS,eAAe;AAC1B,YAAM5B,SAAS,sJAAsJwC,QAAAA;;YAE/J3D,KAAAA;;YAEAxB,KAAAA,GAAQiE,KAAKvB,cAAc;SAAYuB,KAAKvB,WAAW,KAAK,EAAA;;EAEtEgD,QAAQ1E,IAAI;;;AAGR,UAAI;AACF,cAAM+B,QAAQmD,YACZ,MAAM5D,OAAOW,KACX;UAAEC,MAAM;UAAY1B;UAAO+C;UAAMnE,YAAYsF,QAAQtF;UAAY+E;QAAS,GAC1E1G,iBACAkE,QACA,IAAA,CAAA;AAGJ,cAAMwD,OAAO;aAAI,IAAIrD,IAAIM,WAAWL,OAAOoD,QAAQpD,OAAOqD,aAAarD,OAAOlC,KAAAA,CAAAA;UAC3ER,IAAI,CAACN,OAAOsG,UAAUhB,OAAOtF,EAAAA,CAAAA,EAC7BW,OAAO,CAACX,OAAqBA,OAAOb,MAAAA,EACpCQ,MAAM,GAAGyF,QAAAA;AACZW,eAAOZ,KAAI,GAAIiB,IAAAA;AACf3B,cAAMU,KAAK;UACToB,MAAM;UACNC,OAAOb,QAAQtF,WAAWC,IAAI,CAACC,UAAUA,MAAMP,EAAE;UACjDyG,UAAU,CAAA;UACVL;UACA,GAAGM,YAAY1D,KAAAA;QACjB,CAAA;MACF,SAASiD,QAAQ;AACfD,aAAKC,MAAAA;MACP;IACF,OAAO;AACL,YAAMU,YAAY/G,KAAKC,IAAI,GAAG,KAAKwB,QAAQsF,aAAa,CAAA;AACxD,YAAMC,WAAWhH,KAAKC,IAAI,GAAG,KAAKwB,QAAQuF,YAAY,CAAA;AACtD,UAAIC,WAAW3C,KAAKpD;AACpB,UAAIgG,gBAA0B,CAAA;AAC9B,eAASP,OAAO,GAAGM,SAASnH,SAAS,KAAKqG,OAAOrG,SAAS0F,UAAUmB,QAAQ;AAC1E,YAAIA,OAAOK,UAAU;AACnB/B,sBAAY;AACZ;QACF;AACA,cAAM2B,QAAQ3F,IACZgG,SAASvG,IAAI,CAACrB,UAAU;UAAEA;QAAK,EAAA,GAC/BoG,kBAAAA;AAEF,cAAMK,OAAOJ,MAAMhB,IAAIuC,SAAS,CAAA,GAAI7G,MAAM,EAAA,GAAK0F,QAAQ,CAAA;AACvD,cAAMqB,QACJrB,KAAKhG,SAAS,IAAI;kBAAqBgG,KAAKpF,IAAI,CAACrB,SAASA,KAAKgB,KAAK,EAAEF,KAAK,KAAA,CAAA,KAAW;AACxF,cAAM6C,SAAS;mLAC4J+D,SAAAA;;YAEvKlF,KAAAA;;YAEAxB,KAAAA,GAAQiE,KAAKvB,cAAc;SAAYuB,KAAKvB,WAAW,KAAK,EAAA,GAAKoE,KAAAA;;EAE3EP,MAAMvF,IAAI;;;AAGJ,YAAI+B;AACJ,YAAI;AACFA,kBAAQmD,YACN,MAAM5D,OAAOW,KACX;YAAEC,MAAM;YAAY1B;YAAO+C;YAAMnE,YAAYmG,MAAMnG;YAAY+E,UAAUuB;UAAU,GACnFjI,iBACAkE,QACA,IAAA,CAAA;QAGN,SAASqD,QAAQ;AACfD,eAAKC,MAAAA;AACL;QACF;AACA,cAAMe,aAAa,IAAIjE,IAAI8D,SAASvG,IAAI,CAACrB,SAASA,KAAKe,EAAE,CAAA;AACzD,cAAMiH,OAAO,wBAACC,UACZ;aAAI,IAAInE,IAAIM,WAAW6D,KAAAA,CAAAA;UACpB5G,IAAI,CAACN,OAAOsG,UAAUhB,OAAOtF,EAAAA,CAAAA,EAC7BW,OAAO,CAACX,OAAqBA,OAAOb,UAAa6H,WAAWzD,IAAIvD,EAAAA,CAAAA,GAHxD;AAIb,YAAImH,SAASF,KAAKjE,OAAOmE,MAAAA;AACzB,YAAIf,OAAOa,KAAKjE,OAAOoD,IAAAA;AAEvBA,eAAO;aACFA;aACAe,OAAOxG,OAAO,CAACX,QAAQsF,MAAMhB,IAAItE,EAAAA,GAAKf,KAAKkB,SAAST,UAAU,OAAO,CAAA;;AAE1EyH,iBAASA,OAAOxG,OAAO,CAACX,QAAQsF,MAAMhB,IAAItE,EAAAA,GAAKf,KAAKkB,SAAST,UAAU,KAAK,CAAA;AAC5E,cAAM0H,UAAU;aAAID;aAAWf;UAAMzG,MAAM,GAAGgH,SAAAA;AAC9CQ,iBAASA,OAAOxG,OAAO,CAACX,OAAOoH,QAAQC,SAASrH,EAAAA,CAAAA;AAChDoG,eAAO;aAAI,IAAIrD,IAAIqD,KAAKzF,OAAO,CAACX,OAAOoH,QAAQC,SAASrH,EAAAA,CAAAA,CAAAA;;AACxDyE,cAAMU,KAAK;UACToB;UACAC,OAAOA,MAAMnG,WAAWC,IAAI,CAACC,UAAUA,MAAMP,EAAE;UAC/CyG,UAAUU;UACVf;UACA,GAAGM,YAAY1D,KAAAA;QACjB,CAAA;AACA+C,eAAOZ,KAAI,GAAIiB,IAAAA;AACfU,wBAAgBK;AAChBN,mBAAWM,OAAOG,QAAQ,CAACtH,OAAOsF,MAAMhB,IAAItE,EAAAA,GAAKf,KAAKkB,YAAY,CAAA,CAAE;AACpE,YAAI0G,SAASnH,WAAW,GAAG;AACzBoH,0BAAgB,CAAA;QAClB;MACF;AAEA,UAAIjC,cAAc1F,QAAW;AAC3B4G,eAAOZ,KAAI,GAAI2B,aAAAA;MACjB;IACF;AAIA,UAAMS,SAAS;SAAI,IAAIxE,IAAIgD,MAAAA;;AAC3B,UAAMyB,WAAWD,OAAO5G,OACtB,CAACX,OACC,CAACuH,OAAOE,KACN,CAACC,UAAUA,UAAU1H,MAAMsF,MAAMhB,IAAIoD,KAAAA,GAAQhC,KAAK+B,KAAK,CAACxI,SAASA,KAAKe,OAAOA,EAAAA,CAAAA,CAAAA;AAGnF,UAAMc,QAAQ0G,SAAS7H,MAAM,GAAGyF,QAAAA,EAAUkC,QAAQ,CAACtH,OAAAA;AACjD,YAAMO,QAAQ+E,MAAMhB,IAAItE,EAAAA;AACxB,UAAIO,UAAUpB,QAAW;AACvB,eAAO,CAAA;MACT;AACA,YAAM,EAAEF,MAAMyG,KAAI,IAAKnF;AACvB,aAAO;QACL;UACEoH,QAAQ1I,KAAKe;UACbC,OAAOhB,KAAKgB;UACZyF,MAAM;eAAIA;YAAMzG;YAAMqB,IAAI,CAACiG,UAAU;YAAEvG,IAAIuG,KAAKvG;YAAIC,OAAOsG,KAAKtG;UAAM,EAAA;UACtE,GAAIhB,KAAKC,cAAcC,SAAY;YAAED,WAAWD,KAAKC;UAAU,IAAI,CAAC;UACpE,GAAID,KAAKG,YAAYD,SAAY;YAAEC,SAASH,KAAKG;UAAQ,IAAI,CAAC;UAC9DwI,WAAW3I,KAAK2I;UAChBC,SAAS5I,KAAK4I;QAChB;;IAEJ,CAAA;AACA,WAAO;MACLvF,YAAY4B,KAAK5B;MACjB,GAAI4B,KAAKjE,UAAUd,SAAY;QAAEc,OAAOiE,KAAKjE;MAAM,IAAI,CAAC;MACxDuE;MACAC;MACA3D;MACA4D,UAAUnC,OAAOuF;MACjBnD,aAAapC,OAAOoC;MACpBC,cAAcrC,OAAOqC;MACrB,GAAIC,cAAc1F,SAAY;QAAE0F;MAAU,IAAI,CAAC;MAC/C,GAAIlB,UAAUxE,SAAY;QAAEwE;MAAM,IAAI,CAAC;IACzC;EACF;EAEA,MAAcoB,WAAWb,MAAoBK,YAAoD;AAC/F,UAAMwD,kBAAkB,KAAK1G,QAAQ0G,mBAAmB;AACxD,UAAMC,WAAWpI,KAAKC,IAAI,GAAG,KAAKwB,QAAQ4G,mBAAmB,EAAA;AAC7D,UAAMzE,YAAYe,WAAWE,MAC1BnE,IAAI,CAACiG,SAASA,KAAK/C,SAAS,EAC5B7C,OAAO,CAACM,SAAyBA,SAAS9B,MAAAA,EAC1CY,KAAK,IAAA;AACR,UAAM6B,WAAsB,CAAA;AAC5B,eAAW,CAACsG,MAAMjJ,IAAAA,KAASsF,WAAWzD,MAAMqH,QAAO,GAAI;AACrD,YAAMC,QAAQ,MAAM,KAAKjH,MAAMkH,UAC7BnE,KAAK5B,YACLrD,KAAK2I,WACLhI,KAAK0I,IAAIrJ,KAAK4I,SAAS5I,KAAK2I,YAAYI,WAAW,CAAA,CAAA;AAErD,UAAI/G,OAAOmH,MAAM9H,IAAI,CAACiI,SAASA,KAAKtH,IAAI,EAAElB,KAAK,MAAA;AAC/C,UAAIkB,KAAKvB,SAASqI,iBAAiB;AACjC9G,eAAO,GAAGA,KAAKtB,MAAM,GAAGoI,kBAAkB,CAAA,CAAA;MAC5C;AACAnG,eAASuD,KAAK;QACZnF,IAAI,GAAGkE,KAAK5B,UAAU,SAASrD,KAAK0I,MAAM;QAC1C1G;;QAEAuH,OAAO,KAAKN,OAAO;QACnBpC,QAAQ5B,KAAK4B,UAAU5B,KAAKjE,SAASiE,KAAK5B;QAC1CmG,UAAU;UACR,GAAGvE,KAAKuE;UACRnG,YAAY4B,KAAK5B;UACjBqF,QAAQ1I,KAAK0I;UACb1H,OAAOhB,KAAKgB;UACZ,GAAIhB,KAAKC,cAAcC,SAAY;YAAED,WAAWD,KAAKC;UAAU,IAAI,CAAC;UACpE,GAAID,KAAKG,YAAYD,SAAY;YAAEC,SAASH,KAAKG;UAAQ,IAAI,CAAC;UAC9DsG,MAAMzG,KAAKyG;UACXgD,WAAW;UACX,GAAIlF,YAAY;YAAEA,WAAWA,UAAU7D,MAAM,GAAG,GAAA;UAAO,IAAI,CAAC;QAC9D;MACF,CAAA;IACF;AACA,WAAOiC;EACT;AACF;AAGA,SAAS0E,UAAUhB,OAA6BqD,KAAW;AACzD,QAAM3I,KAAK2I,IAAIC,KAAI,EAAGC,QAAQ,YAAY,EAAA;AAC1C,MAAIvD,MAAM/B,IAAIvD,EAAAA,GAAK;AACjB,WAAOA;EACT;AACA,MAAI,QAAQ8I,KAAK9I,EAAAA,GAAK;AACpB,UAAM+I,SAAS/I,GAAGgJ,SAAS,GAAG,GAAA;AAC9B,QAAI1D,MAAM/B,IAAIwF,MAAAA,GAAS;AACrB,aAAOA;IACT;EACF;AACA,SAAO5J;AACT;AAZSmH;AAeT,SAASH,YAAYnD,OAAa;AAChC,QAAMiG,QAAOhG,eAAeD,KAAAA;AAC5B,MAAIiG,UAAS9J,QAAW;AACtB,UAAM,IAAI+J,MAAM,kCAAkCC,KAAKC,UAAUpG,MAAMrD,MAAM,GAAG,GAAA,CAAA,CAAA,EAAO;EACzF;AACA,SAAOsJ;AACT;AANS9C;AAQT,SAASO,YAAY1D,OAA0C;AAC7D,QAAMS,WAAWT,OAAOS,YAAYT,OAAOQ;AAC3C,SAAO,OAAOC,aAAa,YAAYA,SAASmF,KAAI,IAChD;IAAEpF,WAAWC,SAAS9D,MAAM,GAAG,GAAA;EAAO,IACtC,CAAC;AACP;AALS+G;;;AC/iBT,SAAS2C,oBAAoBC,SAAgB;AAC3C,QAAMC,eAAeD,QAAQE,UAAUC;AACvC,SAAO,OAAOF,iBAAiB,WAAWA,eAAeG,aAAaJ,QAAQK,EAAE;AAClF;AAHSN;AAoBF,IAAMO,oBAAN,MAAMA;EAhDb,OAgDaA;;;;;;EACX,YACmBC,YACAC,WACAC,UAAoC,CAAC,GACtD;SAHiBF,aAAAA;SACAC,YAAAA;SACAC,UAAAA;EAChB;EAEHC,oBAAyC;AACvC,WAAO;MAAEC,WAAW;IAAY;EAClC;EAEA,MAAMC,SAASC,OAAeJ,UAA2B,CAAC,GAAuB;AAC/E,YAAQ,MAAM,KAAKK,OAAOD,OAAOJ,OAAAA,GAAUM;EAC7C;;EAGA,MAAMD,OACJD,OACAJ,UAA2B,CAAC,GACyC;AACrE,UAAMO,OAAOP,QAAQO,QAAQ;AAC7B,UAAMC,QAAQ,KAAKR,QAAQL,gBAAgBL;AAC3C,UAAMmB,QAAQ,MAAM,KAAKX,WAAWK,SAASC,OAAO;MAClDG,MAAMG,KAAKC,IAAIJ,MAAM,KAAKP,QAAQY,aAAa,EAAA;MAC/C,GAAIZ,QAAQa,WAAWC,SAAY;QAAED,QAAQb,QAAQa;MAAO,IAAI,CAAC;IACnE,CAAA;AACA,UAAME,SAAmB,CAAA;AACzB,eAAWxB,WAAWkB,OAAO;AAC3B,YAAMb,KAAKY,MAAMjB,OAAAA;AACjB,UAAI,CAACwB,OAAOC,SAASpB,EAAAA,GAAK;AACxBmB,eAAOE,KAAKrB,EAAAA;MACd;IACF;AACA,UAAMsB,aAAaH,OAAOI,MAAM,GAAGT,KAAKC,IAAI,GAAG,KAAKX,QAAQoB,qBAAqB,CAAA,CAAA;AACjF,UAAMC,YAAY,IAAIC,KAElB,MAAM,KAAKvB,UAAUwB,MAAMC,KAAK;MAC9BC,aAAaP;MACb,GAAIlB,QAAQa,WAAWC,SAAY;QAAED,QAAQb,QAAQa;MAAO,IAAI,CAAC;IACnE,CAAA,GACAa,IAAI,CAACC,WAAWA,OAAOjC,UAAU,CAAA;AAErC,UAAMkC,aAAaV,WAChBL,OAAO,CAACjB,OAAOyB,UAAUQ,IAAIjC,EAAAA,CAAAA,EAC7BuB,MAAM,GAAGT,KAAKC,IAAI,GAAG,KAAKX,QAAQ8B,qBAAqB,CAAA,CAAA;AAE1D,UAAMC,YAAY,oBAAIC,IAAAA;AACtB,QAAIC,cAAoC,CAAA;AACxC,QAAIL,WAAWM,SAAS,GAAG;AACzB,YAAMC,SAAS,MAAM,KAAKpC,UAAUqC,SAAShC,OAAOwB,YAAY;QAC9D,GAAI5B,QAAQa,WAAWC,SAAY;UAAED,QAAQb,QAAQa;QAAO,IAAI,CAAC;QACjE,GAAI,KAAKb,QAAQqC,WAAWvB,SAAY;UAAEuB,QAAQ,KAAKrC,QAAQqC;QAAO,IAAI,CAAC;MAC7E,CAAA;AACAJ,oBAAcE,OAAOG;AACrB,iBAAW/C,WAAW4C,OAAO7B,UAAU;AACrC,cAAMV,KAAK2C,OAAOhD,QAAQE,UAAUC,cAAc,EAAA;AAClDqC,kBAAUS,IAAI5C,IAAI;aAAKmC,UAAUU,IAAI7C,EAAAA,KAAO,CAAA;UAAKL;SAAQ;MAC3D;AACA,iBAAWmD,cAAcT,aAAa;AACpC,aAAKjC,QAAQ2C,eAAeD,UAAAA;MAC9B;IACF;AAEA,UAAME,SAAoB,CAAA;AAC1B,UAAMC,UAAU,oBAAIvB,IAAAA;AACpB,eAAW/B,WAAWkB,OAAO;AAC3B,YAAMb,KAAKY,MAAMjB,OAAAA;AACjB,YAAMuD,cAAcf,UAAUU,IAAI7C,EAAAA;AAClC,UAAIkD,gBAAgBhC,UAAagC,YAAYZ,WAAW,GAAG;AACzDU,eAAO3B,KAAK1B,OAAAA;AACZ;MACF;AACA,UAAI,CAACsD,QAAQhB,IAAIjC,EAAAA,GAAK;AACpBiD,gBAAQE,IAAInD,EAAAA;AACZgD,eAAO3B,KAAI,GAAI6B,WAAAA;MACjB;AACA,UAAI,KAAK9C,QAAQgD,sBAAsB;AACrCJ,eAAO3B,KAAK1B,OAAAA;MACd;IACF;AACA,WAAO;MAAEe,UAAUsC,OAAOzB,MAAM,GAAGZ,IAAAA;MAAO0B;IAAY;EACxD;AACF;;;ACzFA,SAASgB,QAAQC,OAAc;AAC7B,SACE,OAAOA,UAAU,YACjBA,UAAU,QACV,OAAQA,MAAkCC,eAAe,YACzD,OAAQD,MAAkCE,aAAa;AAE3D;AAPSH;AAST,IAAMI,cAAuD;EAC3D,aAAa;IACXC,SAAS;IACTC,QAAQ;IACRC,SAASN,OAAc;AACrB,aAAOD,QAAQC,KAAAA,IACX;QAAEA,OAAO;UAAEC,YAAYD,MAAMC;UAAYC,UAAUF,MAAME;QAAS;MAAE,IACpE;QACEK,QAAQ;UACN;YAAEC,SAAS;UAAqE;;MAEpF;IACN;EACF;AACF;AAmBO,SAASC,2BACdC,WACAC,UAAuC,CAAC,GAAC;AAEzC,SAAO;IACLC,MAAM;MACJC,MAAMF,QAAQE,QAAQ;MACtBC,MAAM;MACNC,aACEJ,QAAQI,eACR;MACFZ;IACF;IACAa,SAAS;MACP,MAAMC,QAAQC,OAAgBC,KAAc;AAC1C,YAAI,CAACpB,QAAQmB,KAAAA,GAAQ;AACnB,gBAAM,IAAIE,MAAM,oEAAA;QAClB;AACA,cAAMC,SAASV,QAAQU,SAASF,GAAAA;AAChC,cAAMG,kBAAkB;UACtB,GAAID,WAAWE,SAAY;YAAEF;UAAO,IAAI,CAAC;UACzC,GAAIV,QAAQa,WAAWD,SAAY;YAAEC,QAAQb,QAAQa;UAAO,IAAI,CAAC;QACnE;AACA,YAAIvB,aAAaiB,MAAMjB,WAAWwB,KAAI;AACtC,YAAIC,SAAS,MAAMhB,UAAUiB,SAAST,MAAMhB,UAAU;UAACD;WAAaqB,eAAAA;AACpE,YAAIM,aAAaF,OAAOG,UAAU,CAAA;AAClC,YAAID,YAAYE,cAAc,aAAa;AACzC,gBAAMC,OAAOC,aAAa/B,WAAWgC,QAAQ,cAAc,EAAA,CAAA;AAC3D,cAAIF,SAAS9B,YAAY;AACvBA,yBAAa8B;AACbL,qBAAS,MAAMhB,UAAUiB,SAAST,MAAMhB,UAAU;cAACD;eAAaqB,eAAAA;AAChEM,yBAAaF,OAAOG,UAAU,CAAA;UAChC;QACF;AACA,YAAID,eAAeL,UAAaK,WAAWE,cAAc,aAAa;AACpE,iBAAO;YACL7B;YACAiC,OAAO;YACPC,UAAU,CAAA;YACVC,OAAO,CAAA;YACPC,OAAO,CAAA;YACPC,MAAM;UACR;QACF;AACA,eAAO;UACLrC;UACAiC,OAAO;UACPC,UAAUT,OAAOS;UACjBC,OAAOR,WAAWQ;UAClBC,OAAOT,WAAWS;UAClB,GAAIT,WAAWE,cAAcP,SAAY;YAAEO,WAAWF,WAAWE;UAAU,IAAI,CAAC;UAChF,GAAIF,WAAWQ,MAAMG,WAAW,IAC5B;YAAED,MAAM;UAA+D,IACvE,CAAC;QACP;MACF;IACF;EACF;AACF;AA1DgB7B;","names":["chunkText","text","options","chunkSize","overlap","normalized","trim","length","separator","undefined","chunkByRecord","chunks","start","end","Math","min","breakAt","window","slice","boundary","max","lastIndexOf","chunk","push","current","flush","record","split","trimmed","candidate","isLexicalVectorStore","store","searchText","UnsafeRemovalError","Error","reason","message","name","chunkIndexOf","chunkId","match","exec","undefined","Number","documentIdOf","replace","splitMetadataPatch","patch","set","remove","key","value","Object","entries","push","undefined","isEmptyMetadataPatch","keys","length","applyMetadataPatch","metadata","next","matchesFilter","metadata","filter","entries","Object","undefined","length","every","key","expected","Array","isArray","actual","some","value","includes","filterMatchesNothing","values","MemoryVectorStore","records","Map","describeRetrieval","store","upsert","record","set","id","remove","documentId","keys","documentIdOf","delete","updateMetadata","patch","isEmptyMetadataPatch","updated","metadata","applyMetadataPatch","listDocuments","filter","documents","values","undefined","matchesFilter","has","listChunks","options","chunks","push","index","chunkIndexOf","text","sort","a","b","offset","limit","slice","listDocumentIds","filterMatchesNothing","Set","add","removeMany","documentIds","length","wanted","removeWhere","Object","UnsafeRemovalError","removed","countChunks","count","search","embedding","scored","score","cosineSimilarity","source","topK","dot","normA","normB","Math","min","valueA","valueB","denominator","sqrt","words","s","Set","split","filter","Boolean","DEFAULT_STOP_WORDS","english","portuguese","spanish","TOKEN","keywordTerms","query","max","stopWords","tokens","toLowerCase","normalize","match","counts","Object","entries","map","stop","t","has","length","best","Math","n","slice","terms","some","hasSearchSyntax","test","anyTermTsquery","replace","join","ADAPTED","Symbol","for","toPgClient","source","isDrizzle","$client","unsafe","fromPostgresJs","fromQueryable","client","undefined","mark","Object","defineProperty","value","rowsOf","result","Array","isArray","rows","sql","query","text","params","begin","bind","transaction","work","Promise","resolve","tx","isNodePgPool","connection","connect","broken","runInTransaction","release","Error","isNodePgClient","onRollbackFailed","error","catch","totalCount","processID","connectionParameters","NUL_BYTE","String","fromCharCode","stripNulBytes","value","includes","split","join","Array","isArray","map","item","proto","Object","getPrototypeOf","prototype","fromEntries","entries","key","entry","PgVectorStore","client","table","dimensions","indexedDimensions","nullableEmbeddings","iterativeScan","efSearch","upsertBatchSize","iterativeScanSupport","options","toPgClient","width","Number","isInteger","Error","undefined","Set","Math","max","floor","describeRetrieval","store","collection","schemaStatements","mixed","column","statements","push","ensureSchema","statement","query","upsert","records","byId","Map","record","id","delete","set","unique","values","start","length","batch","slice","params","rows","text","source","metadata","base","JSON","stringify","embedding","toVectorLiteral","remove","documentId","DOCUMENT_ID_FROM_CHUNK","updateMetadata","patch","isEmptyMetadataPatch","splitMetadataPatch","listDocuments","filter","where","row","doc_id","listChunks","sql","CHUNK_INDEX_FROM_ID","limit","offset","index","chunk_index","listDocumentIds","filterMatchesNothing","removeMany","documentIds","removeWhere","keys","UnsafeRemovalError","countChunks","count","search","topK","conditions","whereConditions","vectorType","withSearchSettings","toPassage","buildWhere","read","transaction","bind","iterative","supportsIterativeScan","tx","then","major","minor","extversion","catch","PgLexicalVectorStore","tsConfig","tsColumn","anyTermFallback","stopWords","config","fullText","SQL_IDENTIFIER","test","DEFAULT_STOP_WORDS","tsvector","searchText","trim","hasSearchSyntax","strict","runTextSearch","terms","keywordTerms","runRankedTermSearch","tsquery","any","anyTermTsquery","score","stripped","clauses","scalar","keyParam","arrParam","RedisVectorSchemaMismatchError","Error","index","field","expected","actual","remedy","name","RedisVectorStore","prefix","dimensions","filterableFields","lexicalScorer","client","options","describeRetrieval","store","collection","ensureSchema","info","sendCommand","createIndex","reconcileSchema","attributes","parseIndexAttributes","size","vector","get","undefined","missing","existing","push","type","schema","String","upsert","records","record","args","id","text","encodeVector","embedding","source","metadata","JSON","stringify","value","encodeTag","remove","documentId","keys","chunkKeys","length","updateMetadata","patch","isEmptyMetadataPatch","updated","key","raw","current","parseMetadata","toStr","next","applyMetadataPatch","stale","Set","bare","exists","Number","add","pattern","escapeGlob","cursor","reply","page","parseScanReply","listChunks","ordered","map","slice","entry","chunkIndexOf","sort","a","b","offset","limit","chunks","fields","Array","isArray","listDocuments","filter","filterMatchesNothing","query","buildFilter","documents","Map","batchSize","passages","parseSearchReply","passage","documentIdOf","has","set","values","listDocumentIds","SCAN_PAGE","parseSearchKeys","stripPrefix","removeMany","documentIds","wanted","pending","DELETE_BATCH","removeWhere","Object","UnsafeRemovalError","unindexed","includes","named","join","declared","removed","deleted","isFinite","countChunks","parseSearchTotal","search","topK","searchText","terms","tokenizeQuery","filterQuery","textQuery","startsWith","results","readId","total","total_results","readInfoField","readKeyValues","attribute","identifier","dim","toUpperCase","toLowerCase","entries","Buffer","from","Float32Array","buffer","clauses","alternation","escapeTag","match","replace","char","rawKeys","isBuffer","toString","scoreSource","stride","rawFields","attrs","inlineScore","toPassage","result","readAttributes","readScore","score","extra_attributes","rawId","vector_score","metadata_json","parsed","parse","describesRetrieval","value","Reflect","get","describeRetrieval","candidate","describeSource","base","retriever","_ignored","source","describeSharedSource","legs","sources","map","first","undefined","agrees","every","store","collection","EmbeddingRetriever","embedder","store","options","describeRetrieval","retriever","describeSource","retrieve","query","embedding","embed","undefined","passages","search","topK","filter","minScore","passage","score","KeywordRetriever","docs","Map","documentFrequency","totalLength","k1","b","options","describeRetrieval","retriever","size","add","records","record","removeChunk","id","tokens","tokenize","text","frequencies","token","set","get","term","keys","length","remove","documentId","documentIdOf","clear","existing","undefined","next","delete","retrieve","query","total","averageLength","queryTerms","Set","scored","values","filter","matchesFilter","metadata","score","termFrequency","df","idf","Math","log","denominator","push","source","sort","a","slice","topK","toLowerCase","match","LexicalRetriever","store","describeRetrieval","retriever","describeSource","retrieve","query","options","searchText","topK","filter","undefined","HybridRetriever","retrievers","options","describeRetrieval","retriever","describeSharedSource","retrieve","query","k","fetchTopK","weights","lists","Promise","all","map","topK","filter","undefined","fused","Map","forEach","list","listIndex","weight","passage","rank","contribution","existing","get","id","score","set","values","sort","a","b","slice","entry","RerankingRetriever","base","reranker","options","describeRetrieval","retriever","describeSource","retrieve","query","candidates","topK","fetchTopK","filter","undefined","rerank","FilteredRetriever","base","filter","describeRetrieval","retriever","describeSource","retrieve","query","options","chunkDocuments","documents","options","chunks","document","chunkText","text","forEach","index","push","id","source","undefined","metadata","ingestChunks","length","embeddings","embedder","embed","map","chunk","records","embedding","store","upsert","ingestDocuments","RAG_DIAGNOSTIC_LIB","RAG_RETRIEVAL_EVENT","RAG_RETRIEVAL_CHANNEL","channelName","emitRagRetrieval","event","durationMs","emit","DEFAULT_TOP_K","inRetrieval","AsyncLocalStorage","round4","value","Math","round","scoreStats","passages","scores","map","passage","score","filter","Number","isFinite","length","sum","reduce","total","topScore","max","meanScore","InstrumentedRetriever","base","options","describeRetrieval","describe","retrieve","query","getChannel","hasSubscribers","getStore","descriptor","topK","startedAt","Date","now","run","retriever","store","undefined","collectionOf","chunks","zeroHit","failed","error","Error","message","String","collection","resolved","instrumentRetriever","isQueryInput","value","query","querySchema","version","vendor","validate","issues","message","createRetrievalTool","retriever","options","topK","telemetry","instrumented","instrumentRetriever","spec","name","kind","description","inputSchema","handler","execute","input","Error","passages","retrieve","undefined","HttpModelError","Error","status","message","name","isBatchTooLarge","error","test","learnedBatchLimits","Map","openAiEmbeddings","options","url","baseUrl","replace","limitKey","model","configured","Math","max","floor","batchSize","min","get","maxInputChars","timeoutMs","doFetch","fetch","request","input","response","method","headers","apiKey","authorization","body","JSON","stringify","dimensions","undefined","signal","AbortSignal","timeout","text","ok","errorMessageOf","json","parseJson","items","data","sort","a","b","index","length","every","item","Array","isArray","embedding","tokens","usage","total_tokens","prompt_tokens","Number","isFinite","onUsage","map","requestSplitting","vectors","start","push","slice","half","ceil","set","onWarn","embed","texts","parse","HttpReranker","format","doFetch","options","fetch","rerank","query","passages","length","topK","documents","map","passage","text","response","url","method","headers","apiKey","authorization","body","JSON","stringify","model","undefined","top_n","texts","signal","AbortSignal","timeout","timeoutMs","ok","HttpModelError","status","errorMessageOf","items","itemsOf","seen","Set","reranked","item","index","has","add","push","score","Number","relevance_score","sort","a","b","slice","json","parse","Array","isArray","wrapped","results","data","walkTree","nodes","path","node","children","indexTree","tree","index","Map","entry","set","id","MARKDOWN","KEYWORD_LEVEL","part","chapter","title","appendix","annex","exhibit","schedule","subpart","article","item","section","KEYWORD","NUMBERED","clean","replace","trim","looksLikeBody","rest","test","split","length","DANGLING","Set","dangling","last","pop","toLowerCase","has","detectHeadings","texts","markdownOnly","headings","inFence","run","dropped","closeRun","forEach","entry","position","heading","index","keep","keyword","undefined","level","add","text","unit","seenText","lines","map","raw","at","line","startsWith","atUnitStart","markdown","exec","push","candidate","joined","next","slice","allCaps","toUpperCase","numbered","Math","min","isPageFurniture","isTitleCase","dropContentsDuplicates","dropRunningHeaders","filter","_","Map","set","key","get","words","word","runningLines","counts","seen","threshold","max","ceil","count","unitCount","TreeBudgetExceededError","Error","limit","name","estimateTokens","text","Math","ceil","length","BudgetedLlm","calls","inputTokens","outputTokens","exhausted","deadline","llm","budget","outerSignal","timeoutMs","undefined","Date","now","Number","POSITIVE_INFINITY","remainingMs","fits","prompt","maxOutputTokens","limitHit","maxCalls","maxInputTokens","min","aborted","call","task","system","outputCap","max","callTimeoutMs","reservedInput","controller","AbortController","onOuterAbort","abort","reason","addEventListener","once","timer","response","Promise","race","signal","_","reject","isFinite","setTimeout","usage","error","clearTimeout","removeEventListener","parseJsonReply","tryParse","candidate","value","JSON","parse","Array","isArray","trimmed","trim","direct","fenced","exec","inner","start","indexOf","end","lastIndexOf","slice","stringList","map","item","String","filter","NOOP_SINK","write","fail","treeLlmFromModelProvider","provider","request","result","runTurn","messages","role","content","tools","sink","abortSignal","openAiChatTreeLlm","options","url","baseUrl","replace","doFetch","fetch","json","kind","method","headers","apiKey","authorization","body","stringify","model","temperature","max_tokens","jsonMode","response_format","type","ok","HttpModelError","status","errorMessageOf","parsed","choices","message","prompt_tokens","completion_tokens","cachedTreeLlm","cache","Map","key","stableHash","hit","get","set","h1","h2","i","c","charCodeAt","imul","toString","padStart","DEFAULT_BUDGET","maxCalls","maxInputTokens","maxOutputTokens","timeoutMs","callTimeoutMs","STRUCTURE_SYSTEM","SUMMARY_SYSTEM","toUnits","input","pages","undefined","sections","Error","units","map","section","index","text","page","titled","some","title","trim","declared","flatMap","unit","level","Math","max","min","atUnitStart","kind","outlineHeadings","headings","entry","outline","findIndex","candidate","length","push","floor","sort","a","b","nestHeadings","unitCount","roots","stack","forEach","heading","end","next","node","unitStart","unitEnd","children","pop","parent","first","unshift","firstLine","running","line","split","find","test","has","replace","toLowerCase","slice","rangeLabel","start","last","groupNodes","groupSize","maxChildren","levelsLeft","from","to","lead","depth","parents","per","ceil","descendantTitles","nodes","into","walkDraft","shape","options","shaped","maxDepth","keyItems","only","maxLeafUnits","merged","previous","spanText","parts","join","ownTexts","texts","head","probe","at","indexOf","excerptOf","total","reduce","sum","used","part","collapse","flat","llmHeadings","llm","perUnit","windowChars","windows","current","size","window","shown","body","prompt","reply","parseJsonReply","call","Array","isArray","Number","isInteger","mapLimit","items","limit","work","results","workers","Promise","all","buildDocumentTree","started","Date","now","minHeadings","llmEnabled","summaries","describe","llmStructure","detect","detectHeadings","summaryMinChars","summaryInputChars","unitHashes","stableHash","fingerprint","JSON","stringify","v","metadata","_metadata","source","_source","tree","unchanged","budget","BudgetedLlm","reject","signal","fallbackReason","structure","runningLines","found","structureWindowChars","maxStructureCalls","error","TreeBudgetExceededError","message","draft","counter","finalize","id","String","padStart","pageStart","pageEnd","contentHash","previousSummaries","Map","walkTree","summary","summarySource","set","breadthFirst","queue","shift","concat","reusedSummaries","pending","reused","get","own","summariesCut","concurrency","childTitles","child","excerpt","fits","json","description","outlineForDescription","documentId","version","stats","llmCalls","calls","inputTokens","outputTokens","durationMs","budgetExhausted","exhausted","builtAt","toISOString","STOP","Set","split","terms","text","toLowerCase","match","filter","term","length","has","overlap","query","score","keywordTreeLlm","request","task","kind","ranked","candidates","map","candidate","order","title","summary","entry","sort","a","b","depth","slice","maxNodes","mode","JSON","stringify","thinking","read","c","id","expand","hasChildren","documents","document","description","maxDocuments","flat","replace","trim","sentence","exec","outline","join","sections","unit","units","line","trimmed","markdown","undefined","push","level","test","usage","inputTokens","Math","ceil","prompt","outputTokens","headerOf","tree","documentId","title","undefined","description","structure","fingerprint","unitCount","metadata","source","builtAt","passes","filter","Object","keys","length","filterMatchesNothing","matchesFilter","MemoryDocumentTreeStore","trees","Map","put","units","set","structuredClone","map","unit","get","entry","getMany","documentIds","id","Set","push","list","options","ids","headers","values","has","sort","a","b","localeCompare","limit","slice","readUnits","start","end","Math","max","remove","delete","IDENTIFIER","json","value","undefined","JSON","parse","PgDocumentTreeStore","client","table","unitsTable","insertBatchSize","options","toPgClient","name","test","Error","stringify","Math","max","floor","schemaStatements","index","replace","ensureSchema","statement","query","whereConditions","filter","params","buildWhere","put","tree","units","clean","stripNulBytes","metadata","rest","write","documentId","start","length","batch","slice","rows","map","unit","push","page","text","at","join","title","description","structure","fingerprint","unitCount","source","builtAt","transaction","bind","get","getMany","documentIds","Set","conditions","row","list","limit","headerOf","document_id","version","Number","unit_count","nodes","stats","llmCalls","inputTokens","outputTokens","durationMs","budgetExhausted","reusedSummaries","built_at","Date","toISOString","String","readUnits","end","idx","remove","work","indexDocumentTree","input","options","store","minUnits","minChars","build","units","toUnits","chars","reduce","total","unit","text","length","remove","documentId","status","reason","previous","get","built","buildDocumentTree","undefined","unchanged","metadataMoved","JSON","stringify","metadata","tree","source","put","NAVIGATE_SYSTEM","DEFAULT_BUDGET","maxCalls","maxInputTokens","maxOutputTokens","timeoutMs","pages","node","pageStart","undefined","pageEnd","candidate","summaryChars","depth","shownPages","summary","length","slice","Math","max","keyItems","join","id","title","hasChildren","children","render","candidates","map","entry","indent","repeat","meta","filter","Boolean","fit","nodes","maxChars","last","text","TreeNavigationRetriever","store","llm","options","describeRetrieval","retriever","retrieve","query","result","search","passages","topK","maxDocuments","maxCandidates","maxSelectionCandidates","headers","list","limit","navigate","header","documentId","budget","BudgetedLlm","signal","documents","description","prompt","document","known","Set","reply","parseJsonReply","call","kind","selected","stringList","answer","has","reasoning","thinking","selection","error","message","documentIds","ids","trees","Map","getMany","tree","navigations","mapLimit","concurrency","get","navigation","mode","steps","llmCalls","inputTokens","outputTokens","stoppedBy","walk","passagesOf","maxContextChars","used","passage","push","maxNodes","singlePassMaxChars","index","indexTree","whole","walkTree","path","outline","Number","POSITIVE_INFINITY","source","chosen","stop","caught","TreeBudgetExceededError","requireJson","read","node_list","resolveId","step","shown","expanded","reasoningOf","beamWidth","maxSteps","frontier","pendingExpand","where","onFrontier","pick","value","expand","limited","includes","flatMap","unique","specific","some","other","nodeId","unitStart","unitEnd","calls","maxPassageChars","maxUnits","maxUnitsPerNode","rank","entries","units","readUnits","min","unit","score","metadata","retrieval","raw","trim","replace","test","padded","padStart","json","Error","JSON","stringify","defaultDocumentIdOf","passage","fromMetadata","metadata","documentId","documentIdOf","id","TwoStageRetriever","firstStage","navigator","options","describeRetrieval","retriever","retrieve","query","search","passages","topK","docOf","first","Math","max","fetchTopK","filter","undefined","ranked","includes","push","considered","slice","considerDocuments","withTrees","Set","store","list","documentIds","map","header","toNavigate","has","navigateDocuments","navigated","Map","navigations","length","result","navigate","budget","documents","String","set","get","navigation","onNavigation","merged","emitted","replacement","add","keepFirstStageChunks","isInput","value","documentId","question","inputSchema","version","vendor","validate","issues","message","createNavigateDocumentTool","navigator","options","spec","name","kind","description","handler","execute","input","ctx","Error","filter","navigateOptions","undefined","budget","trim","result","navigate","navigation","documents","stoppedBy","bare","documentIdOf","replace","found","passages","nodes","steps","note","length"]}
|