Artifact Contracts¶
Ingest produces two durable handoff formats: JSONL chunk streams and MessagePack retrieval indexes. The formats solve different problems. JSONL exposes records for inspection and exchange; an index preserves backend-specific state for fast loading and querying.
flowchart LR
Source[CSV source records] --> Pipeline[clean, chunk, embed]
Pipeline --> JSONL[chunk JSONL]
Pipeline --> Build[index builder]
Build --> BM25[BM25 MessagePack]
Build --> Dense[NumPy cosine MessagePack]
BM25 --> Load[validated loader]
Dense --> Load
Load --> Query[retrieval]
Chunk JSONL¶
The document pipeline writes one successful chunk per line. Rendered errors go to the command output and do not become JSONL rows. A consumer that needs to prove completeness must therefore retain the process outcome and reconcile the produced rows with the expected inputs.
Two CLI paths expose different projections:
| Writer | Structural fields | Metadata shape | Embedding |
|---|---|---|---|
| document shell | text |
metadata keys flattened into the row | JSON array |
| pipeline result writer | doc_id, text, start, end |
nested under metadata |
JSON array |
The document-shell projection can overwrite metadata keys named text or
embedding with the structural values. Neither projection includes the full
retrieval chunk: title, category, ordinal, embedding specification, and derived
chunk_id are absent. JSONL is consequently an inspection and exchange
boundary, not a lossless index checkpoint.
Preserve the source file, effective chunking configuration, writer identity, process outcome, and embedding identity beside the output when the file is intended for replay. A vector alone does not identify the model or normalization policy that created it.
The projections are easiest to distinguish by shape. Values below are illustrative; field placement is the contract being shown.
{"doc_id":"policy-17","text":"retained text","start":0,"end":13,"metadata":{"title":"Policy"},"embedding":[0.0,1.0]}
Neither writer emits a file header, schema discriminator, producer version, configuration digest, source digest, row count, or terminal success marker. Those omissions are why a standalone JSONL file cannot prove pipeline completion or select its own projection reliably.
Retrieval Indexes¶
BM25Index.save() and NumpyCosineIndex.save() write MessagePack payloads.
Both payloads carry:
schema_version, which the loader checks exactly;backend, which prevents loading one representation as another;- chunk content, source offsets, metadata, and stored
chunk_idvalues; and - the backend state required to reproduce scoring.
The BM25 payload also records token buckets, term frequencies, document frequencies, document lengths, and scoring parameters. The dense payload records the embedding specification plus the vector dtype, shape, and bytes. Loading recomputes each chunk identity and rejects a mismatch as possible corruption.
An index fingerprint covers its schema, backend configuration, ordered chunk identities, and numerical state. Use it to identify an exact built index, not as a substitute for retaining the input and build configuration.
Acceptance Checks¶
Before accepting a JSONL handoff, validate its projection, every row, the expected success count, and the embedding specification supplied out of band. Before accepting an index, load it through the matching backend loader and record the recomputed fingerprint. A successful MessagePack decode alone does not establish backend, schema, chunk-identity, or vector-shape validity.
The loaders validate the schema and backend discriminators and recompute stored chunk identities. Dense loading also reconstructs the declared vector array. It does not authenticate the file or impose a general input-size limit. Apply an external digest or signature and a resource limit when artifacts cross a trust boundary.
flowchart TD
bytes["candidate artifact bytes"]
envelope{"trusted digest, size limit,<br/>and expected format known?"}
decode["decode JSONL or MessagePack"]
validate["validate projection, schema,<br/>backend, identities, and shapes"]
reconcile["reconcile source count,<br/>failures, and build receipt"]
accept["accepted handoff"]
reject["reject or quarantine"]
bytes --> envelope
envelope -->|no| reject
envelope -->|yes| decode
decode --> validate
validate -->|invalid| reject
validate -->|valid| reconcile
reconcile -->|incomplete| reject
reconcile -->|complete| accept
The backend loaders cover the validation node, not the external envelope or
source reconciliation nodes. Calling load() successfully is necessary but
not sufficient for accepting an artifact received from another trust domain.
Ownership and Safe Publication¶
The caller chooses the destination path. save_stored_index() reports an
expected persistence failure as Err[str]; the surrounding workflow remains
responsible for directory creation, access control, retention, and atomic
publication. Do not expose index files from an untrusted source: MessagePack is
decoded into typed internal structures, but payload size and array dimensions
still consume local resources.
Treat these changes as artifact-breaking:
- changing field meaning or numeric dtype without a new schema version;
- changing chunk ordering or stable-identity inputs;
- accepting a backend under the wrong discriminator; or
- removing validation that currently rejects corrupt identifiers or unsupported versions.
See Data Contracts for the in-process record shapes and State and Persistence for storage ownership.