v0.6.0
Minor Changes
-
e819cea: Ingest: add
parseDocument, a stateless entry point that turns an uploaded file into a uniformParsedDocthe existing chunkers accept as-is (additive, no breaking changes). It normalizes four whitelisted formats — PDF text layer (application/pdf), Word documents (docx), Markdown (text/markdown) and plain text (text/plain) — delegating every parse to a mature library (PDF via the existingparsePdf, docx viamammoth); no parser is hand-rolled. PDF documents keep their 1-indexed page numbers ondoc.pages(ready forchunkPdfPages); docx/Markdown/text become heading-preserving text ondoc.text(ready forchunk, whose heading tracker turns#–####into asectionprovenance path). Markdown and plain-text bytes are decoded as UTF-8 first with an automatic GBK fallback — no charset-detection dependency, using the Node-built-in decoder — and the detected encoding is reported ondoc.encoding.Failure is returned, never thrown, so a single bad file never crashes a batch ingest: the result is a discriminated union
{ ok: true; doc } | { ok: false; error; message }carrying a stableINGEST_ERROR_CODEScode —UNSUPPORTED_MIME_TYPE,PARSE_FAILED(corrupt / encrypted / malformed input),PARSE_TIMEOUT(a configurabletimeoutMshard ceiling, defaultDEFAULT_PARSE_TIMEOUT_MS) orEMPTY_DOCUMENT. The function is side-effect-free and never mutates or detaches its input buffer. ExportsparseDocument,DEFAULT_PARSE_TIMEOUT_MS,INGEST_ERROR_CODES, and theParsedDoc/ParseResult/ParseDocumentOptions/SupportedMimeType/IngestErrorCodetypes. -
89a903f: Knowledge graph: add stateless primitives to grow an entity / relation graph from already-indexed chunks and store it alongside the vectors and full-text index (additive, no breaking changes). The graph tables live in the same
.dbfile as the existingdocs/ vector / FTS tables, so the graph travels with that index version; rebuilding the corpus into a fresh version starts with no graph.buildGraphSchema(db)— idempotent DDL (IF NOT EXISTS) that addsentities,relationsand their*_mentionsback-link tables in place. Safe to call on any already-built index to opt it into graph storage; it never touches the core tables, and a graph-less database still opens read-only unchanged.extractGraph({ chunks, extractFn })— pure orchestration that turns chunks into a deduplicated graph. Extraction is fully injected viaextractFn(wire it to any LLM provider, a rule-based extractor, or a test stub) — the toolkit ships no default extractor and depends on no model SDK. Entities and relations collapse onto a deterministic, NFKC-based normalized key so re-extracting the same corpus produces no duplicate nodes; each node and edge back-links to the source chunk(s) it was mentioned in; relation endpoints are registered as nodes so the graph is always connected; and a single chunk whoseextractFnthrows (or resolves a malformed result) is skipped and counted infailedChunksrather than aborting the batch.writeGraph(db, graph)— persists an extracted graph in one transaction, idempotently: entities and relations useINSERT OR IGNOREagainst aUNIQUEnormalized key and mention rows use a composite primary key, so writing the same graph twice inserts nothing and leaves node / edge counts unchanged.
extractGraphaccepts an optionalonSpancallback that emits oneingest.graphspan carrying scalar counts only (chunk / entity / relation / failed counts) — never names or chunk content — so it is safe to export to an observability backend. Uses only Node built-ins and the existing SQLite dependency; no new dependencies. ExportsbuildGraphSchema,extractGraph,writeGraph,GRAPH_ERROR_CODES,GraphError, and theGraphChunk/ExtractFn/RawEntity/RawRelation/RawExtraction/ExtractedEntity/ExtractedRelation/ExtractedGraph/GraphExtractionOptions/GraphStats/GraphErrorCodetypes. -
0c1faaa: Observability: the retrieval and ingest pipelines can now emit structured
PipelineSpanevents through an optional, injectedonSpancallback, so you can bridge pipeline timing to any observability backend without the toolkit depending on a tracing SDK (additive, no breaking changes).Pass
onSpanon the per-call options of the bound hybrid search (createHybridSearch), the bound reranker (createReranker),parseDocument, orIndexStore.buildVersion. Each pipeline stage then emits one span:createHybridSearchemits aretrieve.hybridparent span withretrieve.bm25/retrieve.vector/retrieve.rrfchild spans (each child'sparentIdis the hybrid span'sid).createRerankeremits aretrieve.rerankspan (including a zero-candidate span when the input list is empty).parseDocumentemits oneingest.parsespan per call, for successful and failed parses alike.IndexStore.buildVersionemits aningest.indexspan on a successful build.
Each span carries a name, an id and optional
parentId,startedAtEpochMs,durationMs, and a scalar-onlyattributesmap (counts, scores, dimensions, format enums, error codes). Spans are metadata only — never query text, chunk content, source or file names — so they are safe to export to an external backend. Pass an outer span id viaparentSpanIdto graft the toolkit's spans under your own trace.When no
onSpanis provided the pipelines read no clock, mint no id and allocate no span object, so the hot path is unchanged and there is zero overhead. Uses only Node built-ins (node:crypto,performance,Date) — no new dependencies. Exports thePipelineSpan,PipelineSpanName,OnSpanandSpanAttributeValuetypes. -
e819cea: RAG store: add
openIndexStore, a versioned wrapper overopenIndexthat lets a running service re-index a corpus and switch over without interrupting in-flight queries (additive, no breaking changes —openIndexand its handle are untouched). It keeps per-corpus version files (v<N>.db) plus a singlemanifest.jsonpointer, and exposesbuildVersion(rows)(full rebuild into a fresh version file — decoupled from going live, so a slow rebuild never stalls current queries),swap(version?)(atomically flip the serving pointer via a temp-file +rename, so a reader or a crash never sees a half-written manifest),rollback()(step the pointer back one version), andwithHandle(fn)for running a query against the current version.Handle lifetime is reference-counted: a query that started before a
swapkeeps reading the pre-swap version to completion, and that version's connection is only closed once its last in-flight reader releases it. Old version files are pruned to a configurablekeepVersionswindow (default 2), but the current version, its one-step rollback target, and any version with an in-flight reader are never deleted — a reader-held version's.db(with its WAL/SHM sidecars) is unlinked only after the reader releases. Building an empty corpus is refused, and a malformed or inconsistentmanifest.jsonfails fast with a typedIndexStoreError(err.code, e.g.EMPTY_CORPUS/CORRUPT_MANIFEST/VERSION_NOT_FOUND). Zero new dependencies — purenode:fsover the existingbetter-sqlite3path. ExportsopenIndexStore,IndexStoreError,INDEX_STORE_ERROR_CODES, and theIndexStore/IndexStoreOptions/BuildVersionResult/IndexStoreErrorCodetypes.