@arnilo/prism-rag 0.3.0 → 0.3.2
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/CHANGELOG.md +10 -0
- package/dist/chunk.js +32 -3
- package/dist/fusion.d.ts +18 -0
- package/dist/fusion.js +0 -0
- package/dist/hash.d.ts +6 -0
- package/dist/hash.js +9 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/indexing.js +33 -3
- package/dist/limits.d.ts +4 -0
- package/dist/limits.js +4 -0
- package/dist/parsers.js +156 -16
- package/dist/retrieve.js +262 -92
- package/dist/sources.d.ts +2 -0
- package/dist/sources.js +65 -3
- package/dist/tei-reranker.d.ts +35 -0
- package/dist/tei-reranker.js +138 -0
- package/dist/telemetry.d.ts +16 -0
- package/dist/telemetry.js +2 -0
- package/dist/types.d.ts +35 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.2] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Plan 035-039 changed-package cut: additive runtime performance, tooling, and documentation deltas; peer window refresh.
|
|
7
|
+
|
|
8
|
+
## [0.3.1] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Hybrid retrieve (`lexical` / RRF / `retrieval` labels), multi-scope retrieve (`scopes: RagScope[]` — one embed, per-scope legs, one RRF, one rerank; `scope` stays valid), content-hash skip, heading-stack chunk metadata, generation stamping, `RagTelemetry` seam, `createTeiReranker`, `fuseReciprocalRank`, `isValidContentHash`. `HARD_RETRIEVE_SCOPE_CAP`. Provenance carries `tenantId`/`resourceId`/`corpusId`. Retrieve fails closed on embedder id/dimension drift (`ERR_PRISM_RAG_EMBEDDER_MISMATCH`).
|
|
12
|
+
|
|
3
13
|
## [0.1.0] - 2026-08-09
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/dist/chunk.js
CHANGED
|
@@ -18,12 +18,26 @@ function chunkDocument(text, options, markdown) {
|
|
|
18
18
|
if (text.length > limits.maxDocumentChars) {
|
|
19
19
|
throw new RagLimitError(`document exceeds ${limits.maxDocumentChars} characters`);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const callerMeta = options.metadata;
|
|
22
|
+
if (callerMeta)
|
|
23
|
+
assertBytes(callerMeta, 64 * 1024, "chunk metadata");
|
|
23
24
|
if (!text.trim())
|
|
24
25
|
return Object.freeze([]);
|
|
26
|
+
const headings = [];
|
|
27
|
+
if (markdown) {
|
|
28
|
+
let pos = 0;
|
|
29
|
+
for (const line of text.split("\n")) {
|
|
30
|
+
const trimmed = line.trimStart();
|
|
31
|
+
const match = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
32
|
+
if (match)
|
|
33
|
+
headings.push({ pos, level: match[1].length, text: match[2].trim() });
|
|
34
|
+
pos += line.length + 1; // +1 for the newline character
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
const chunks = [];
|
|
26
38
|
let start = 0;
|
|
39
|
+
let headingIdx = 0;
|
|
40
|
+
const headingStack = [];
|
|
27
41
|
while (start < text.length) {
|
|
28
42
|
while (start < text.length && /\s/.test(text[start]))
|
|
29
43
|
start += 1;
|
|
@@ -33,8 +47,23 @@ function chunkDocument(text, options, markdown) {
|
|
|
33
47
|
const end = ceiling === text.length ? ceiling : preferredEnd(text, start, ceiling, markdown);
|
|
34
48
|
const raw = text.slice(start, end).trimEnd();
|
|
35
49
|
if (raw) {
|
|
50
|
+
// Advance heading stack to the current chunk start position.
|
|
51
|
+
while (headingIdx < headings.length && headings[headingIdx].pos <= start) {
|
|
52
|
+
const h = headings[headingIdx];
|
|
53
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= h.level) {
|
|
54
|
+
headingStack.pop();
|
|
55
|
+
}
|
|
56
|
+
headingStack.push({ level: h.level, text: h.text });
|
|
57
|
+
headingIdx++;
|
|
58
|
+
}
|
|
36
59
|
const index = chunks.length;
|
|
37
60
|
const citationId = `${sourceId}#${String(index + 1).padStart(4, "0")}`;
|
|
61
|
+
const meta = callerMeta ? { ...callerMeta } : {};
|
|
62
|
+
// Stamp heading metadata unless the caller supplied explicit heading data.
|
|
63
|
+
if (markdown && meta.heading === undefined && headingStack.length > 0) {
|
|
64
|
+
meta.heading = headingStack.map((h) => h.text);
|
|
65
|
+
}
|
|
66
|
+
const metadata = Object.keys(meta).length > 0 ? Object.freeze(meta) : undefined;
|
|
38
67
|
chunks.push(Object.freeze({
|
|
39
68
|
id: citationId,
|
|
40
69
|
citationId,
|
|
@@ -43,7 +72,7 @@ function chunkDocument(text, options, markdown) {
|
|
|
43
72
|
start,
|
|
44
73
|
end: start + raw.length,
|
|
45
74
|
text: raw,
|
|
46
|
-
...(
|
|
75
|
+
...(metadata ? { metadata } : {}),
|
|
47
76
|
}));
|
|
48
77
|
if (chunks.length > limits.maxChunks)
|
|
49
78
|
throw new RagLimitError(`chunk count exceeds ${limits.maxChunks}`);
|
package/dist/fusion.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MemoryVectorHit } from "@arnilo/prism-memory";
|
|
2
|
+
/** Which retrieval leg(s) surfaced a hit before fusion. */
|
|
3
|
+
export type RetrievalLeg = "vector" | "lexical";
|
|
4
|
+
export interface FusedCandidate {
|
|
5
|
+
readonly hit: MemoryVectorHit;
|
|
6
|
+
readonly retrieval: RetrievalLeg | "hybrid";
|
|
7
|
+
}
|
|
8
|
+
/** N-list RRF. Fusion key includes scope so the same id in two corpora stays two hits. */
|
|
9
|
+
export declare function fuseReciprocalRankLists(lists: readonly {
|
|
10
|
+
readonly hits: readonly MemoryVectorHit[];
|
|
11
|
+
readonly leg: RetrievalLeg;
|
|
12
|
+
}[], rrfK: number): readonly FusedCandidate[];
|
|
13
|
+
/**
|
|
14
|
+
* Reciprocal-rank fusion: score(id) = Σ 1/(rrfK + rank) over each list containing the id.
|
|
15
|
+
* Pure and store-free; ties break by best rank, then id, so output order is deterministic.
|
|
16
|
+
* When a record appears in both legs the vector-leg hit object wins (display score stays cosine).
|
|
17
|
+
*/
|
|
18
|
+
export declare function fuseReciprocalRank(vectorHits: readonly MemoryVectorHit[], lexicalHits: readonly MemoryVectorHit[], rrfK: number): readonly FusedCandidate[];
|
package/dist/fusion.js
ADDED
|
Binary file
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document hashes are supplied by the host (never computed over unbounded bytes inside the
|
|
3
|
+
* engine). Accept any hex digest of 32..128 characters so hosts can pick the algorithm;
|
|
4
|
+
* comparison is exact-match on the normalized lowercase form.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isValidContentHash(value: unknown): value is string;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document hashes are supplied by the host (never computed over unbounded bytes inside the
|
|
3
|
+
* engine). Accept any hex digest of 32..128 characters so hosts can pick the algorithm;
|
|
4
|
+
* comparison is exact-match on the normalized lowercase form.
|
|
5
|
+
*/
|
|
6
|
+
export function isValidContentHash(value) {
|
|
7
|
+
return typeof value === "string" && /^[0-9a-f]{32,128}$/.test(value);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=hash.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
2
2
|
export { createRagContextProvider } from "./context.js";
|
|
3
3
|
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
4
|
+
export type { FusedCandidate, RetrievalLeg } from "./fusion.js";
|
|
5
|
+
export { fuseReciprocalRank } from "./fusion.js";
|
|
6
|
+
export { isValidContentHash } from "./hash.js";
|
|
4
7
|
export { indexChunks } from "./indexing.js";
|
|
5
8
|
export { createMemoryIngestionStatusStore, listIngestionStatus } from "./ingestion-status.js";
|
|
6
9
|
export type { RagLimits, RagLimitsInput } from "./limits.js";
|
|
7
|
-
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_EMBED_BATCH_SIZE, DEFAULT_INGESTION_STATUS_PAGE_SIZE, DEFAULT_MAX_CHUNKS, DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_DOCUMENT_CHARS, DEFAULT_MAX_METADATA_BYTES, DEFAULT_MAX_PARSE_MS, DEFAULT_MAX_PDF_PAGES, DEFAULT_MAX_RERANK_BYTES, DEFAULT_MAX_RERANK_MS, DEFAULT_MAX_RESULT_BYTES, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_QUERY_CANDIDATES, DEFAULT_RERANK_CONCURRENCY, DEFAULT_TOP_K, HARD_CHUNK_OVERLAP_CAP, HARD_CHUNK_SIZE_CAP, HARD_EMBED_BATCH_SIZE_CAP, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, HARD_MAX_CHUNKS_CAP, HARD_MAX_CONTEXT_TOKENS_CAP, HARD_MAX_DOCUMENT_BYTES_CAP, HARD_MAX_DOCUMENT_CHARS_CAP, HARD_MAX_METADATA_BYTES_CAP, HARD_MAX_PARSE_MS_CAP, HARD_MAX_PDF_PAGES_CAP, HARD_MAX_RERANK_BYTES_CAP, HARD_MAX_RERANK_MS_CAP, HARD_MAX_RESULT_BYTES_CAP, HARD_QUERY_CANDIDATES_CAP, HARD_RERANK_CONCURRENCY_CAP, HARD_TOP_K_CAP, resolveRagLimits, } from "./limits.js";
|
|
10
|
+
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_EMBED_BATCH_SIZE, DEFAULT_INGESTION_STATUS_PAGE_SIZE, DEFAULT_MAX_CHUNKS, DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_DOCUMENT_CHARS, DEFAULT_MAX_METADATA_BYTES, DEFAULT_MAX_PARSE_MS, DEFAULT_MAX_PDF_PAGES, DEFAULT_MAX_RERANK_BYTES, DEFAULT_MAX_RERANK_MS, DEFAULT_MAX_RESULT_BYTES, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_QUERY_CANDIDATES, DEFAULT_RERANK_CONCURRENCY, DEFAULT_TOP_K, HARD_CHUNK_OVERLAP_CAP, HARD_CHUNK_SIZE_CAP, HARD_EMBED_BATCH_SIZE_CAP, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, HARD_MAX_CHUNKS_CAP, HARD_MAX_CONTEXT_TOKENS_CAP, HARD_MAX_DOCUMENT_BYTES_CAP, HARD_MAX_DOCUMENT_CHARS_CAP, HARD_MAX_METADATA_BYTES_CAP, HARD_MAX_PARSE_MS_CAP, HARD_MAX_PDF_PAGES_CAP, HARD_MAX_RERANK_BYTES_CAP, HARD_MAX_RERANK_MS_CAP, HARD_MAX_RESULT_BYTES_CAP, HARD_QUERY_CANDIDATES_CAP, HARD_RERANK_CONCURRENCY_CAP, HARD_RETRIEVE_SCOPE_CAP, HARD_TOP_K_CAP, resolveRagLimits, } from "./limits.js";
|
|
8
11
|
export { createResourceDocumentLoader, createWebFetchDocumentLoader } from "./loaders.js";
|
|
9
12
|
export { htmlParser, markdownParser, pdfParser, textParser } from "./parsers.js";
|
|
10
13
|
export { retrieveContext } from "./retrieve.js";
|
|
11
14
|
export type { SourceMutationResult } from "./sources.js";
|
|
12
15
|
export { deleteSource, replaceDocument, replaceSource } from "./sources.js";
|
|
13
|
-
export type {
|
|
16
|
+
export type { CreateTeiRerankerOptions } from "./tei-reranker.js";
|
|
17
|
+
export { createTeiReranker } from "./tei-reranker.js";
|
|
18
|
+
export type { RagTelemetry, RagTelemetryAttributeValue, RagTelemetrySpan } from "./telemetry.js";
|
|
19
|
+
export type { Chunker, ChunkOptions, DeleteSourceOptions, DocumentLoader, DocumentLoadOptions, DocumentParseOptions, IndexChunksOptions, IndexChunksResult, IngestionState, IngestionStatus, IngestionStatusQuery, IngestionStatusStore, LoadedDocument, ParsedDocument, Parser, RagChunk, RagCitation, RagContentTrust, RagContextProvider, RagContextProviderOptions, RagContextResult, RagHit, RagProvenance, RagScope, ReplaceDocumentOptions, ReplaceSourceOptions, Reranker, RetrieveContextOptions, ReusableEmbedding, SourceVectorStore, TransactionalVectorStore, } from "./types.js";
|
|
14
20
|
export declare const packageName = "@arnilo/prism-rag";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
2
2
|
export { createRagContextProvider } from "./context.js";
|
|
3
3
|
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
4
|
+
export { fuseReciprocalRank } from "./fusion.js";
|
|
5
|
+
export { isValidContentHash } from "./hash.js";
|
|
4
6
|
export { indexChunks } from "./indexing.js";
|
|
5
7
|
export { createMemoryIngestionStatusStore, listIngestionStatus } from "./ingestion-status.js";
|
|
6
|
-
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_EMBED_BATCH_SIZE, DEFAULT_INGESTION_STATUS_PAGE_SIZE, DEFAULT_MAX_CHUNKS, DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_DOCUMENT_CHARS, DEFAULT_MAX_METADATA_BYTES, DEFAULT_MAX_PARSE_MS, DEFAULT_MAX_PDF_PAGES, DEFAULT_MAX_RERANK_BYTES, DEFAULT_MAX_RERANK_MS, DEFAULT_MAX_RESULT_BYTES, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_QUERY_CANDIDATES, DEFAULT_RERANK_CONCURRENCY, DEFAULT_TOP_K, HARD_CHUNK_OVERLAP_CAP, HARD_CHUNK_SIZE_CAP, HARD_EMBED_BATCH_SIZE_CAP, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, HARD_MAX_CHUNKS_CAP, HARD_MAX_CONTEXT_TOKENS_CAP, HARD_MAX_DOCUMENT_BYTES_CAP, HARD_MAX_DOCUMENT_CHARS_CAP, HARD_MAX_METADATA_BYTES_CAP, HARD_MAX_PARSE_MS_CAP, HARD_MAX_PDF_PAGES_CAP, HARD_MAX_RERANK_BYTES_CAP, HARD_MAX_RERANK_MS_CAP, HARD_MAX_RESULT_BYTES_CAP, HARD_QUERY_CANDIDATES_CAP, HARD_RERANK_CONCURRENCY_CAP, HARD_TOP_K_CAP, resolveRagLimits, } from "./limits.js";
|
|
8
|
+
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_EMBED_BATCH_SIZE, DEFAULT_INGESTION_STATUS_PAGE_SIZE, DEFAULT_MAX_CHUNKS, DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_DOCUMENT_CHARS, DEFAULT_MAX_METADATA_BYTES, DEFAULT_MAX_PARSE_MS, DEFAULT_MAX_PDF_PAGES, DEFAULT_MAX_RERANK_BYTES, DEFAULT_MAX_RERANK_MS, DEFAULT_MAX_RESULT_BYTES, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_QUERY_CANDIDATES, DEFAULT_RERANK_CONCURRENCY, DEFAULT_TOP_K, HARD_CHUNK_OVERLAP_CAP, HARD_CHUNK_SIZE_CAP, HARD_EMBED_BATCH_SIZE_CAP, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, HARD_MAX_CHUNKS_CAP, HARD_MAX_CONTEXT_TOKENS_CAP, HARD_MAX_DOCUMENT_BYTES_CAP, HARD_MAX_DOCUMENT_CHARS_CAP, HARD_MAX_METADATA_BYTES_CAP, HARD_MAX_PARSE_MS_CAP, HARD_MAX_PDF_PAGES_CAP, HARD_MAX_RERANK_BYTES_CAP, HARD_MAX_RERANK_MS_CAP, HARD_MAX_RESULT_BYTES_CAP, HARD_QUERY_CANDIDATES_CAP, HARD_RERANK_CONCURRENCY_CAP, HARD_RETRIEVE_SCOPE_CAP, HARD_TOP_K_CAP, resolveRagLimits, } from "./limits.js";
|
|
7
9
|
export { createResourceDocumentLoader, createWebFetchDocumentLoader } from "./loaders.js";
|
|
8
10
|
export { htmlParser, markdownParser, pdfParser, textParser } from "./parsers.js";
|
|
9
11
|
export { retrieveContext } from "./retrieve.js";
|
|
10
12
|
export { deleteSource, replaceDocument, replaceSource } from "./sources.js";
|
|
13
|
+
export { createTeiReranker } from "./tei-reranker.js";
|
|
11
14
|
export const packageName = "@arnilo/prism-rag";
|
|
12
15
|
//# sourceMappingURL=index.js.map
|
package/dist/indexing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveRedactor } from "@arnilo/prism";
|
|
2
2
|
import { RagValidationError } from "./errors.js";
|
|
3
|
+
import { isValidContentHash } from "./hash.js";
|
|
3
4
|
import { ingestionStatus } from "./ingestion-status.js";
|
|
4
5
|
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
5
6
|
import { assertBytes, assertNotAborted, byteLength, nonEmpty, requireScope, requireSourceId } from "./util.js";
|
|
@@ -20,6 +21,7 @@ export async function indexChunkBatches(options, write) {
|
|
|
20
21
|
options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
21
22
|
throw new RagValidationError(`embedder dimensions must be an integer in 1..${limits.maxVectorDimensions}`);
|
|
22
23
|
}
|
|
24
|
+
nonEmpty(options.embedder.id, "embedder.id");
|
|
23
25
|
if (options.chunks.length > limits.maxChunks)
|
|
24
26
|
throw new RagValidationError(`chunk count exceeds ${limits.maxChunks}`);
|
|
25
27
|
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
@@ -59,13 +61,29 @@ export async function indexChunkBatches(options, write) {
|
|
|
59
61
|
}
|
|
60
62
|
};
|
|
61
63
|
await setStatus("pending");
|
|
64
|
+
const embedSpan = options.telemetry?.startSpan("embedding.index", { "rag.embedder_id": options.embedder.id }, options.telemetryParent);
|
|
62
65
|
try {
|
|
63
66
|
for (let offset = 0; offset < options.chunks.length; offset += limits.embedBatchSize) {
|
|
64
67
|
assertNotAborted(options.signal);
|
|
65
68
|
const batch = options.chunks.slice(offset, offset + limits.embedBatchSize);
|
|
66
69
|
const texts = batch.map((chunk) => redactor?.redact(chunk.text) ?? chunk.text);
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Embed only the delta: chunks whose id+text match a stored record reuse its embedding.
|
|
71
|
+
const vectors = batch.map((chunk, index) => {
|
|
72
|
+
const reused = options.reuseEmbeddings?.get(chunk.id);
|
|
73
|
+
if (!reused || reused.text !== texts[index])
|
|
74
|
+
return undefined;
|
|
75
|
+
return [...reused.embedding];
|
|
76
|
+
});
|
|
77
|
+
const pending = vectors.flatMap((vector, index) => (vector === undefined ? [index] : []));
|
|
78
|
+
if (pending.length) {
|
|
79
|
+
const fresh = await options.embedder.embed(pending.map((index) => texts[index]), { signal: options.signal });
|
|
80
|
+
if (fresh.length !== pending.length)
|
|
81
|
+
throw new RagValidationError("embedder returned unexpected vector count");
|
|
82
|
+
pending.forEach((index, position) => {
|
|
83
|
+
vectors[index] = [...fresh[position]];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (vectors.some((vector) => vector === undefined))
|
|
69
87
|
throw new RagValidationError("embedder returned unexpected vector count");
|
|
70
88
|
const records = batch.map((chunk, index) => {
|
|
71
89
|
const embedding = vectors[index];
|
|
@@ -75,7 +93,14 @@ export async function indexChunkBatches(options, write) {
|
|
|
75
93
|
const safeMetadata = redactor?.redact(chunk.metadata ?? {}) ?? chunk.metadata ?? {};
|
|
76
94
|
const metadata = {
|
|
77
95
|
...safeMetadata,
|
|
78
|
-
_rag: {
|
|
96
|
+
_rag: {
|
|
97
|
+
sourceId: chunk.sourceId,
|
|
98
|
+
citationId: chunk.citationId,
|
|
99
|
+
chunkIndex: chunk.index,
|
|
100
|
+
start: chunk.start,
|
|
101
|
+
end: chunk.end,
|
|
102
|
+
...(options.contentHash && isValidContentHash(options.contentHash) ? { contentHash: options.contentHash.toLowerCase() } : {}),
|
|
103
|
+
},
|
|
79
104
|
};
|
|
80
105
|
assertBytes(metadata, limits.maxMetadataBytes, "chunk metadata");
|
|
81
106
|
return {
|
|
@@ -86,6 +111,7 @@ export async function indexChunkBatches(options, write) {
|
|
|
86
111
|
text: texts[index],
|
|
87
112
|
embedding,
|
|
88
113
|
sequence: chunk.index,
|
|
114
|
+
embedderId: options.embedder.id,
|
|
89
115
|
metadata: metadata,
|
|
90
116
|
createdAt: new Date(0).toISOString(),
|
|
91
117
|
};
|
|
@@ -99,9 +125,13 @@ export async function indexChunkBatches(options, write) {
|
|
|
99
125
|
await setStatus("indexed");
|
|
100
126
|
}
|
|
101
127
|
catch (error) {
|
|
128
|
+
embedSpan?.recordError();
|
|
102
129
|
await setStatus([...written.values()].some((progress) => progress.chunks > 0) ? "partial" : "failed", error);
|
|
103
130
|
throw error;
|
|
104
131
|
}
|
|
132
|
+
finally {
|
|
133
|
+
embedSpan?.end();
|
|
134
|
+
}
|
|
105
135
|
return Object.freeze({ indexed: options.chunks.length, sourceIds: Object.freeze([...sourceIds].sort()) });
|
|
106
136
|
}
|
|
107
137
|
function add(target, sourceId, bytes, chunks) {
|
package/dist/limits.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ export declare const DEFAULT_MAX_RERANK_MS = 2000;
|
|
|
31
31
|
export declare const HARD_MAX_RERANK_MS_CAP = 10000;
|
|
32
32
|
export declare const DEFAULT_RERANK_CONCURRENCY = 2;
|
|
33
33
|
export declare const HARD_RERANK_CONCURRENCY_CAP = 8;
|
|
34
|
+
export declare const DEFAULT_RRF_K = 60;
|
|
35
|
+
export declare const HARD_RRF_K_CAP = 1000;
|
|
36
|
+
export declare const HARD_RETRIEVE_SCOPE_CAP = 8;
|
|
34
37
|
export declare const DEFAULT_INGESTION_STATUS_PAGE_SIZE = 50;
|
|
35
38
|
export declare const HARD_INGESTION_STATUS_PAGE_SIZE_CAP = 200;
|
|
36
39
|
export interface RagLimits {
|
|
@@ -51,6 +54,7 @@ export interface RagLimits {
|
|
|
51
54
|
readonly maxRerankBytes: number;
|
|
52
55
|
readonly maxRerankMs: number;
|
|
53
56
|
readonly rerankConcurrency: number;
|
|
57
|
+
readonly rrfK: number;
|
|
54
58
|
readonly ingestionStatusPageSize: number;
|
|
55
59
|
}
|
|
56
60
|
export type RagLimitsInput = Partial<RagLimits>;
|
package/dist/limits.js
CHANGED
|
@@ -32,6 +32,9 @@ export const DEFAULT_MAX_RERANK_MS = 2_000;
|
|
|
32
32
|
export const HARD_MAX_RERANK_MS_CAP = 10_000;
|
|
33
33
|
export const DEFAULT_RERANK_CONCURRENCY = 2;
|
|
34
34
|
export const HARD_RERANK_CONCURRENCY_CAP = 8;
|
|
35
|
+
export const DEFAULT_RRF_K = 60;
|
|
36
|
+
export const HARD_RRF_K_CAP = 1_000;
|
|
37
|
+
export const HARD_RETRIEVE_SCOPE_CAP = 8;
|
|
35
38
|
export const DEFAULT_INGESTION_STATUS_PAGE_SIZE = 50;
|
|
36
39
|
export const HARD_INGESTION_STATUS_PAGE_SIZE_CAP = 200;
|
|
37
40
|
function integer(value, fallback, cap, label, minimum = 1) {
|
|
@@ -70,6 +73,7 @@ export function resolveRagLimits(input = {}) {
|
|
|
70
73
|
maxRerankBytes: integer(input.maxRerankBytes, DEFAULT_MAX_RERANK_BYTES, HARD_MAX_RERANK_BYTES_CAP, "maxRerankBytes"),
|
|
71
74
|
maxRerankMs: integer(input.maxRerankMs, DEFAULT_MAX_RERANK_MS, HARD_MAX_RERANK_MS_CAP, "maxRerankMs"),
|
|
72
75
|
rerankConcurrency: integer(input.rerankConcurrency, DEFAULT_RERANK_CONCURRENCY, HARD_RERANK_CONCURRENCY_CAP, "rerankConcurrency"),
|
|
76
|
+
rrfK: integer(input.rrfK, DEFAULT_RRF_K, HARD_RRF_K_CAP, "rrfK"),
|
|
73
77
|
ingestionStatusPageSize: integer(input.ingestionStatusPageSize, DEFAULT_INGESTION_STATUS_PAGE_SIZE, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, "ingestionStatusPageSize"),
|
|
74
78
|
});
|
|
75
79
|
}
|
package/dist/parsers.js
CHANGED
|
@@ -36,7 +36,19 @@ async function parsePdf(document, options = {}) {
|
|
|
36
36
|
const pages = [...pdf.matchAll(/\/Type\s*\/Page\b/gu)].length;
|
|
37
37
|
if (pages > limits.maxPdfPages)
|
|
38
38
|
throw new RagLimitError(`PDF exceeds ${limits.maxPdfPages} pages`);
|
|
39
|
-
|
|
39
|
+
// Index-scanned BT..ET block extraction instead of /BT[\s\S]*?ET/gu (CodeQL js/polynomial-redos, alert 11).
|
|
40
|
+
const blocks = [];
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
while (cursor < pdf.length) {
|
|
43
|
+
const begin = pdf.indexOf("BT", cursor);
|
|
44
|
+
if (begin === -1)
|
|
45
|
+
break;
|
|
46
|
+
const end = pdf.indexOf("ET", begin + 2);
|
|
47
|
+
if (end === -1)
|
|
48
|
+
break;
|
|
49
|
+
blocks.push(pdf.slice(begin, end + 2));
|
|
50
|
+
cursor = end + 2;
|
|
51
|
+
}
|
|
40
52
|
const text = blocks.flatMap(pdfBlockText).join("\n").trim();
|
|
41
53
|
if (!text)
|
|
42
54
|
throw new RagValidationError("PDF has no uncompressed text");
|
|
@@ -65,17 +77,86 @@ function assertParseTime(started, maxParseMs) {
|
|
|
65
77
|
if (Date.now() - started > maxParseMs)
|
|
66
78
|
throw new RagLimitError(`document parsing exceeded ${maxParseMs}ms`);
|
|
67
79
|
}
|
|
80
|
+
// Single-pass linear HTML-to-text scanner (CodeQL js/incomplete-multi-character-sanitization
|
|
81
|
+
// and js/polynomial-redos on the former regex chain, alerts 17-19): comments, script/style
|
|
82
|
+
// bodies, and tags are consumed by index — hostile adjacency cannot re-form dangerous tags.
|
|
68
83
|
function htmlToText(html) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
84
|
+
const out = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < html.length) {
|
|
87
|
+
if (html.startsWith("<!--", i)) {
|
|
88
|
+
const close = html.indexOf("-->", i + 4);
|
|
89
|
+
i = close === -1 ? html.length : close + 3;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (html.charCodeAt(i) === 60) {
|
|
93
|
+
const gt = html.indexOf(">", i + 1);
|
|
94
|
+
if (gt === -1) {
|
|
95
|
+
out.push(html.slice(i));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const tag = html.slice(i + 1, gt);
|
|
99
|
+
i = gt + 1;
|
|
100
|
+
const closing = tag.startsWith("/");
|
|
101
|
+
const name = (closing ? tag.slice(1) : tag).match(/^[A-Za-z]+/u)?.[0]?.toLowerCase() ?? "";
|
|
102
|
+
if (closing) {
|
|
103
|
+
out.push(" ");
|
|
104
|
+
}
|
|
105
|
+
else if (name === "br" || name === "p" || name === "div" || name === "li" || name === "tr" || /^h[1-6]$/u.test(name)) {
|
|
106
|
+
out.push("\n");
|
|
107
|
+
}
|
|
108
|
+
else if (name === "script" || name === "style") {
|
|
109
|
+
// Consume the raw element body through its matching close tag.
|
|
110
|
+
const closeIdx = html.toLowerCase().indexOf(`</${name}`, i);
|
|
111
|
+
const next = closeIdx === -1 ? html.length : html.indexOf(">", closeIdx);
|
|
112
|
+
i = next === -1 ? html.length : next + 1;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out.push(" ");
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
out.push(html[i++]);
|
|
120
|
+
}
|
|
121
|
+
return collapseTextWhitespace(decodeEntities(out.join(""))).trim();
|
|
122
|
+
}
|
|
123
|
+
/** Linear whitespace normalization: `[ \t]` runs collapse; 3+ newlines collapse to 2; edge spaces drop. */
|
|
124
|
+
function collapseTextWhitespace(text) {
|
|
125
|
+
const out = [];
|
|
126
|
+
let i = 0;
|
|
127
|
+
let lineStart = true;
|
|
128
|
+
let spaceRun = false;
|
|
129
|
+
let newlineRun = 0;
|
|
130
|
+
while (i < text.length) {
|
|
131
|
+
const c = text[i];
|
|
132
|
+
if (c === " " || c === "\t") {
|
|
133
|
+
if (!lineStart)
|
|
134
|
+
spaceRun = true;
|
|
135
|
+
i += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (c === "\n") {
|
|
139
|
+
newlineRun += 1;
|
|
140
|
+
spaceRun = false;
|
|
141
|
+
lineStart = true;
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (newlineRun > 0) {
|
|
146
|
+
out.push("\n".repeat(Math.min(newlineRun, 2)));
|
|
147
|
+
newlineRun = 0;
|
|
148
|
+
}
|
|
149
|
+
else if (spaceRun) {
|
|
150
|
+
out.push(" ");
|
|
151
|
+
spaceRun = false;
|
|
152
|
+
}
|
|
153
|
+
lineStart = false;
|
|
154
|
+
out.push(c);
|
|
155
|
+
i += 1;
|
|
156
|
+
}
|
|
157
|
+
if (newlineRun > 0)
|
|
158
|
+
out.push("\n".repeat(Math.min(newlineRun, 2)));
|
|
159
|
+
return out.join("");
|
|
79
160
|
}
|
|
80
161
|
function decodeEntities(text) {
|
|
81
162
|
const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
|
|
@@ -83,11 +164,70 @@ function decodeEntities(text) {
|
|
|
83
164
|
}
|
|
84
165
|
function pdfBlockText(block) {
|
|
85
166
|
const strings = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
167
|
+
// Linear index scanners replace ambiguous nested-quantifier regexes (CodeQL js/polynomial-redos, alerts 15-16, 20-21).
|
|
168
|
+
const extractLiteral = (text, from) => {
|
|
169
|
+
const open = text.indexOf("(", from);
|
|
170
|
+
if (open === -1)
|
|
171
|
+
return undefined;
|
|
172
|
+
let i = open + 1;
|
|
173
|
+
while (i < text.length) {
|
|
174
|
+
const c = text[i];
|
|
175
|
+
if (c === "\\") {
|
|
176
|
+
i += 2;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (c === ")")
|
|
180
|
+
return { literal: text.slice(open + 1, i), next: i + 1 };
|
|
181
|
+
i += 1;
|
|
182
|
+
}
|
|
183
|
+
return undefined; // unterminated literal - ignore
|
|
184
|
+
};
|
|
185
|
+
let cursor = 0;
|
|
186
|
+
while (cursor < block.length) {
|
|
187
|
+
const literal = extractLiteral(block, cursor);
|
|
188
|
+
if (!literal)
|
|
189
|
+
break;
|
|
190
|
+
let j = literal.next;
|
|
191
|
+
while (j < block.length && /\s/.test(block[j]))
|
|
192
|
+
j += 1;
|
|
193
|
+
if (block[j] === "T" && block[j + 1] === "j") {
|
|
194
|
+
strings.push(decodePdfString(`(${literal.literal})`));
|
|
195
|
+
cursor = j + 2;
|
|
196
|
+
}
|
|
197
|
+
else if (block[j] === "'" || block[j] === '"') {
|
|
198
|
+
strings.push(decodePdfString(`(${literal.literal})`));
|
|
199
|
+
cursor = j + 1;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
cursor = literal.next;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
cursor = 0;
|
|
206
|
+
while (cursor < block.length) {
|
|
207
|
+
const open = block.indexOf("[", cursor);
|
|
208
|
+
if (open === -1)
|
|
209
|
+
break;
|
|
210
|
+
const close = block.indexOf("]", open + 1);
|
|
211
|
+
if (close === -1)
|
|
212
|
+
break;
|
|
213
|
+
let j = close + 1;
|
|
214
|
+
while (j < block.length && /\s/.test(block[j]))
|
|
215
|
+
j += 1;
|
|
216
|
+
if (block.startsWith("TJ", j)) {
|
|
217
|
+
const arr = block.slice(open + 1, close);
|
|
218
|
+
let arrCursor = 0;
|
|
219
|
+
for (;;) {
|
|
220
|
+
const literal = extractLiteral(arr, arrCursor);
|
|
221
|
+
if (!literal)
|
|
222
|
+
break;
|
|
223
|
+
strings.push(decodePdfString(`(${literal.literal})`));
|
|
224
|
+
arrCursor = literal.next;
|
|
225
|
+
}
|
|
226
|
+
cursor = j + 2;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
cursor = open + 1;
|
|
230
|
+
}
|
|
91
231
|
}
|
|
92
232
|
return strings.filter(Boolean);
|
|
93
233
|
}
|