The internal processing pipeline: how raw source material becomes a normalized entity and lands in storage. Three stages — Extract → Transform → Load. Source-specific work covers only the entry (extract) and the first transform step (normalize); everything past that is shared across every source and every sync mode.
fetch(since) → RawItem and emits
a stream of RawItem — that is the entry into
Transform. The fetch mechanics themselves — connection,
authentication, pagination, and the since increment
— are held by
sources.
Entity model. The one transform step written for a
specific source.
draft / final /
archived.
fetch can
return an entity twice. Idempotency
across runs is not here — Load provides it via upsert
on source_id + source_type + source_entity_id. Deep
cross-source entity resolution is held by
data-model.
enrich: links and mentions in the markup are
needed for relationships, and headings for splitting into
fragments. Only the copy bound for embedding is cleaned; the
original in Entity stays untouched.
Entity
→ many fragments, each with its
own vector. Target size — 512 tokens
(working range 256–1024), a tunable parameter.
The model window is not a target but a cap:
effective = min(target_chunk_tokens, model_max_tokens).
A large embedder window (text-embedding-3 has
8192) buys headroom against clipping long
sections, not permission to make a fragment fill the whole
window: the longer the text under a single vector, the more it
“averages out” and the worse search hits become.
normalize: headings, sections, a message
or a thread. The boundary is natural — no overlap
needed.
overlap = 0.
chunk and
embed knobs across two homes. Model intrinsics — the
max window tokens, the required prefix
(query:/passage:), the vector
dimensionality — live in the
AI-model registry;
the chunker reads the cap from there.
Tunable knobs — target_chunk_tokens,
overlap_pct, per-content-type sizes — are in config.
At the seam sits a validator:
target_chunk_tokens ≤ model.max_input_tokens.
source_id + source_type + source_entity_id: each entity is
written to exactly its own row, a repeat run updates it rather
than spawning duplicates. Idempotency extends to fragments too:
their vectors are child rows of the entity, and the same run
reconciles the whole set — stale ones are deleted, new ones
added, no orphaned vectors from a previous version remain.
Unchanged fragments are not re-embedded: the vector is reused by
content hash, and inference hits only what actually changed. The
module's core guarantee: a repeat sync, or connecting a new
source later, does not break already-collected data. One entity
meanwhile lands in several storage sinks at once:
RawItem →
Entity → saved
The pipeline rests on two contracts. Each stage knows only them — which is why everything after normalize stays shared.
RawItemEntityEntity and pass through
the same pipeline. The full model is held by
data-model.