Capability protocols¶
The narrow, runtime_checkable typing.Protocols a Provider implements
(ADR-0004) — no base class or
import of core required — plus the ProviderRegistry that resolves Providers by name.
capability ¶
Capability protocols and the entry-point Provider registry (ADR-0004).
A Capability is a narrow, model-agnostic interface a Stage depends on. A Provider
is a concrete implementation, registered as a plugin under the latence.providers
entry-point group and discovered dynamically via importlib.metadata — the
framework never names a model, only Capabilities.
For S1 the Capabilities are the three spine Stages (Source, Parser, Export). Each
is a typing.Protocol so any object with the right shape satisfies it — no
base class or import of core required for a third-party Provider.
ScreeningOutcome
dataclass
¶
ScreeningOutcome(
passed: list[_Screened] = list(),
quarantined: list[QuarantineRecord] = list(),
findings: list[ScreeningFinding] = list(),
)
Bases: Generic[_Screened]
The result of a Screening checkpoint: what passed, what was Quarantined.
passed are the records that proceed downstream — for Content Screening
these may carry newly-attached risk markers. quarantined are the
:class:~latence_core.contracts.QuarantineRecords removed from the pipeline
(retained for audit, never exported to the corpus). findings is the full
per-record decision log Screening emits into the Quality Report — every
Quarantine reason and every flag, so the disposition is auditable (S3 AC).
Source ¶
Bases: Protocol
Lists and fetches documents from where they live, stamping Provenance.
produce ¶
Yield one raw document per source document, with initial Provenance.
The yielded :class:~latence_core.contracts.ParserInput carries the
undecoded source content (bytes) — decoding and the turn into
markdown belong to the Parser (ADR-0019), not the Source.
Source code in packages/latence-core/src/latence_core/capability.py
DocumentEnumerator ¶
Bases: Protocol
OPTIONAL Source refinement: enumerate document ids WITHOUT fetching content.
A delta run has to know which documents the source currently holds before it can decide
which of them are unchanged and can therefore be skipped (ADR-0018/0043). Deriving that set
from :meth:Source.produce is correct but pays the full fetch cost for every document — for
a remote Source (a SharePoint library, an object store) that means the whole corpus crosses
the network on every delta, which is precisely the cost the delta exists to avoid, and it
happens before the exclusions are known, so no exclude_document_ids can prevent it.
A Source that can name its documents more cheaply than it can fetch them implements this:
SharePoint reads the content hash Graph reports in the listing, so a delta run over an
unchanged library transfers zero bytes. The ids MUST be the same content-addressed ids
:meth:Source.produce stamps on Provenance.document_id — an id that disagreed would
silently mis-classify documents as new. A Source that cannot do better simply does not
implement it, and the runner falls back to produce (correct, just not free).
The enumeration MAY be partial, and a Source that can only name some of its documents
cheaply MUST leave the rest out rather than fetch them: an omitted id cannot intersect the
parent Version, so its document is treated as new and fetched exactly once by produce,
whereas fetching it here transfers it unconditionally in addition to that — which is how a
no-op delta ends up costing twice a full run (audit R3-connectors_ingestion-8). Omitting can
only lose a reuse; it can never invent one, and it never changes a document's records.
document_ids ¶
Yield the Provenance.document_id of every document currently in the source.
Parser ¶
Bases: Protocol
Turns raw source documents into parsed markdown records.
parse ¶
Consume raw :class:ParserInputs and yield parsed markdown records.
Chunker ¶
Bases: Protocol
Splits parsed documents into retrieval-sized ChunkRecords (CONTEXT Chunk).
Preserves offsets, page alignment, Provenance and Classification on every
emitted chunk. A PARSE_ERROR DocumentRecord (no usable content) yields no
chunks.
chunk ¶
Consume parsed :class:DocumentRecords and yield :class:ChunkRecords.
IntakeScreener ¶
Bases: Protocol
Screens raw documents BEFORE Parse (CONTEXT Screening — intake checkpoint).
Catches malware, zip bombs, file-type spoofing, oversized/corrupt files. A dangerous document is Quarantined (removed from every downstream Stage); a clean one passes through untouched.
screen_intake ¶
Partition raw inputs into passed vs quarantined, with findings.
ContentScreener ¶
Bases: Protocol
Screens chunked content AFTER Chunk (CONTEXT Screening — content checkpoint).
Catches prompt injection, harmful content, sensitivity escalation. A flagged
chunk keeps flowing but carries a :class:~latence_core.contracts.RiskMarker
that survives into the corpus; a chunk deemed dangerous is Quarantined.
screen_content ¶
Return chunks (some marked) plus any quarantined chunks and findings.
LabelInducer ¶
Bases: Protocol
Induces a document's own label schema and annotates its chunks (CONTEXT Classification).
The W4 schema-induction Capability (ADR-0038): an OPTIONAL Stage inserted right after
Content Screening and before the extraction Stages. It restores the "point it at a folder and
it discovers its own schema" property — a small OpenAI-compatible LLM with structured JSON
output reads the document text and induces the entity / relation / PII types actually
evidenced in it, then broadcasts that :class:~latence_core.contracts.InducedLabels schema
onto every :class:~latence_core.contracts.ChunkRecord of the document. The extraction Stages
then UNION the induced types onto their config labels (the latence_core.induced seam), so a
Stage with no config labels becomes fully unsupervised and one with config labels is
supervised-plus-augmented — without any extractor rewrite.
It is a chunk→chunk transform (the Content-Screening shape): it consumes the run's chunks
and yields the SAME chunks with induced_labels populated (a doc-level Provider groups chunks
by document_record_id and makes one LLM call per document by default, broadcasting the doc's
induced labels to all its chunks). The Capability names no LLM and no label set — the Provider
supplies both. It is fail-open (G1 posture): any inducer failure (network, malformed output,
truncation, schema-invalid, empty) leaves a chunk's induced_labels None so the extractor
falls back to its config labels and the run never crashes — the untrusted-LLM discipline lives
entirely in the Provider. A chunk it could not annotate simply flows through unchanged.
induce ¶
Consume the run's chunks; yield the same chunks with induced_labels set.
The stream is threaded through 1:1 (never dropped or reordered): a chunk the Provider could
induce labels for carries a populated :class:~latence_core.contracts.InducedLabels; a
chunk whose induction failed (fail-open) flows through with induced_labels still
None. Deterministic + seeded (temperature 0, stable label order) so a run is
byte-reproducible (Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
EntityExtractor ¶
Bases: Protocol
Finds typed entity mentions in chunked text (CONTEXT Entity Extraction).
Zero-shot NER: the label set is configured per Pipeline (Stage config), not
hardcoded — the Capability names no types. Each emitted
:class:~latence_core.contracts.EntityMention carries its label, confidence,
and the char span it occupies in the parent document's assembled markdown,
so a mention resolves back through the parent's page map to its source page(s).
An empty or near-empty chunk yields no mentions (never an error).
extract ¶
Consume :class:ChunkRecords and yield typed :class:EntityMentions.
RelationExtractor ¶
Bases: Protocol
Finds typed relations between entity mentions (CONTEXT Relation Extraction).
Relation Extraction relates mentions within a document (ADR-0022): given the
document's :class:~latence_core.contracts.EntityMentions plus the chunk text
that supplies the surrounding context, it yields directed, typed
:class:~latence_core.contracts.RelationMentions (head → tail). The relation
label set is configured per Pipeline (Stage config), not hardcoded — the
Capability names no relation types (zero-shot / prompt-driven), mirroring
Entity Extraction. Each emitted relation carries the head/tail mention refs, its
label, confidence, and a Provenance span covering both endpoints, so it resolves
back through the parent's page map to its source page(s). A document with fewer
than two mentions, or no relatable pair, yields no relations (never an error).
relate ¶
relate(
chunks: Iterable[ChunkRecord],
mentions: Iterable[EntityMention],
) -> Iterator[RelationMention]
Consume a run's chunks + mentions and yield typed :class:RelationMentions.
Both carriers are supplied: mentions are what a relation connects, and
chunks carry the text (and the parent page map) a text-based Provider
needs for context and offset→page resolution. The Provider groups both by
document_record_id internally — a relation only ever links two mentions
of the same document.
Source code in packages/latence-core/src/latence_core/capability.py
FusedExtraction
dataclass
¶
FusedExtraction(
mentions: list[EntityMention] = list(),
relations: list[RelationMention] = list(),
)
The joint output of a :class:FusedEntityRelationExtractor — one pass, both Stages.
A Fused Provider (ADR-0013: gliner-relex, the custom span-predictor) fulfils
Entity Extraction AND Relation Extraction in a single pass over the chunks, so
it returns both carriers together: the mentions it found and the
relations between them. The Runner threads both downstream from the one
fused Stage — S4's mentions and S5's relations — without invoking a separate
Entity-Extraction Stage (no double-extraction).
FusedEntityRelationExtractor ¶
Bases: Protocol
One Provider that fulfils BOTH Entity + Relation Extraction in one pass (ADR-0013).
The Pipeline does not force one Provider per Stage: a fused model (gliner-relex,
the custom span-predictor) does joint NER + RE, so a single fused Stage produces
both the :class:~latence_core.contracts.EntityMentions and the
:class:~latence_core.contracts.RelationMentions. The Runner routes a fused
Stage's :class:FusedExtraction to both downstream consumers without running a
separate Entity-Extraction Stage — the S5 seam requirement.
extract_fused ¶
Consume :class:ChunkRecords and return mentions + relations together.
PIIDetector ¶
Bases: Protocol
Detects PII per chunk and produces a masked variant (CONTEXT Redaction; W16, ADR-0042).
The S6 Redaction Capability, made chunk-level in W16 to fix the document-level truncation
leak. Before W16 a Provider scanned the WHOLE parsed
:class:~latence_core.contracts.DocumentRecord, feeding it into a fixed PII model window (768
tokens for the gliner-family) — so any PII past that window was SILENTLY truncated and leaked
unmasked into the corpus. Redaction is now a chunk→chunk transform (the Content-Screening /
Context-Enrichment shape): a Provider consumes the run's
:class:~latence_core.contracts.ChunkRecords and yields the SAME chunks with
:attr:~latence_core.contracts.ChunkRecord.masked_content +
:attr:~latence_core.contracts.ChunkRecord.pii_spans populated. Each chunk's text (≤ the model
window) is PII-scanned + masked in full — nothing is truncated, so PII past the old 768-token
document-truncation point is now caught (the ADR-0042 fix).
The chunk's content stays UNMASKED (so a later re-read / audit sees the full entities); the
additive masked_content is the PII-safe variant the Export materializes for the RAG corpus,
and pii_spans are the chunk-local, counts-only detected spans (offsets into the chunk
content, source pages resolved through the chunk's offset_map+page_slice — never the raw
value, S6 AC). Redaction is metadata-aware: the Provider keys its policy off each chunk's
Classification.sensitivity (S6 AC). The PII type set is Provider/config-supplied — the
Capability names no PII types. A chunk with no detected PII yields a chunk whose
masked_content equals its content and whose pii_spans is empty (never an error).
The shared policy/masking/no-op-floor/page-resolution machinery lives in
:mod:latence_core.redaction_policy (plan_chunk_redaction + finalize_redacted_chunk),
so a Provider owns ONLY detection. Because Redaction reads the chunk's markup-stripped
content, a span's chunk-local offset resolves to its ORIGINAL source page through the
chunk's offset_map + the
:class:~latence_core.page_index.PageIndexResolver — the coordinate-correctness ADR-0031/0042
guarantee. The resolver reads the chunk's OWN page_slice (v19), so resolution is exact in
any order and with any sibling chunk missing. A Provider builds ONE resolver per redact
call and hands it to every plan_chunk_redaction (it is a required argument, so the page
seam cannot be forgotten). Redaction therefore depends on the Chunk Stage (not Parse).
The document-level :meth:redact shape (Iterable[DocumentRecord] ->
Iterator[RedactionRecord]) is DEPRECATED; a Provider MAY retain it as a secondary
redact_documents method for the superseded path, but the blessed Capability is the chunk
seam below.
redact ¶
Consume the run's :class:ChunkRecords; yield the SAME chunks with PII masked.
The chunk stream is threaded 1:1 and order-stable (never dropped or reordered): each yielded
chunk carries masked_content (its text with every non-TAG PII span replaced) +
pii_spans (the chunk-local detected spans) + redaction_disabled_for_sensitive (the
H-C1 §4 no-op floor). content is byte-UNCHANGED. Chunks are emitted in first-seen order
for determinism; a seeded run is byte-reproducible (Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
Profiler ¶
Bases: Protocol
Computes corpus-level statistical + quality features (CONTEXT Profiling).
The S7 Profiling Capability. Profiling is a corpus-level Stage: it consumes the
whole run's parsed :class:~latence_core.contracts.DocumentRecords (and, when an
Entity-Extraction Stage is upstream, the run's
:class:~latence_core.contracts.EntityMentions) and emits
:class:~latence_core.contracts.FeatureRecords — one DOCUMENT-scope record per
parsed document carrying its per-document features (density, readability, Zipf α,
compression ratio, structure) and exactly one CORPUS-scope record carrying the
cross-document aggregate features (entity frequency, co-occurrence, type consensus,
source coverage).
Both carriers are supplied so a fused corpus profile can relate document text to the
entities extracted from it; a Provider that only wants document text ignores the
mentions. The feature pass must be streaming / spill-to-disk friendly — it must
not assume the whole corpus fits in RAM (S7 AC) — so a Provider folds documents in
one at a time rather than materialising the corpus. A PARSE_ERROR document (no
usable content) is skipped, mirroring the Chunk Stage. Records are emitted
deterministically (document-scope in first-seen order, then the single corpus-scope
record last), so a seeded run is byte-identical.
profile ¶
profile(
documents: Iterable[DocumentRecord],
mentions: Iterable[EntityMention],
) -> Iterator[FeatureRecord]
Consume the run's documents + mentions; yield per-document + corpus FeatureRecords.
Source code in packages/latence-core/src/latence_core/capability.py
TypeConsolidation
dataclass
¶
TypeConsolidation(
mentions: list[EntityMention] = list(),
relations: list[RelationMention] = list(),
vocabulary: TypeVocabulary | None = None,
)
The output of a :class:TypeConsolidator — remapped records + the vocabulary that did it.
Three things travel together because they are one decision: the mentions and
relations with their label rewritten to canonical form (and their raw induced label
preserved on raw_label), and the :class:~latence_core.contracts.TypeVocabulary that
defines the mapping. The Runner threads the records downstream and persists the vocabulary
beside the Stage's checkpoint — so the consolidation is inspectable as data, and an
incremental run can merge into it (ADR-0017: files are the source of truth).
TypeConsolidator ¶
Bases: Protocol
Canonicalizes the corpus's TYPE vocabulary (CONTEXT Resolution; T3).
The counterpart to :class:Disambiguator in the resolution phase: where a Disambiguator
resolves entity mentions to canonical entities, a TypeConsolidator resolves the
type vocabulary those mentions are labelled with. Both are corpus-level, and both exist
because the front half of the pipeline now runs per chunk: each chunk induces its own
labels, which is what gives it a small, relevant, uncapped label set — and what makes chunk
A say org where chunk B says organization. Consistency is earned HERE, downstream,
instead of being imposed upstream by freezing and salience-capping one corpus schema before
extraction (which silently dropped rare-but-critical labels).
It is a corpus-level mention→mention transform: it consumes the whole run's
:class:~latence_core.contracts.EntityMentions (required — the labels it canonicalizes) and,
optionally, its :class:~latence_core.contracts.RelationMentions, and returns the SAME
records with each label rewritten to its canonical form and the raw induced label kept on
raw_label for audit — plus the :class:~latence_core.contracts.TypeVocabulary it built.
It runs BEFORE Disambiguation, not after: the entity resolver votes a cluster's type from its members' labels and gates its low-precision rungs on type compatibility, so feeding it a drifted vocabulary would both split clusters that belong together and mistype the ones that survive. Canonical types in, consistent entities and consistent graph out.
Determinism is part of the contract: identical inputs must yield an identical vocabulary and identical records (stable clustering, stable canonical election), so a seeded run is byte-identical (Baseline bar).
consolidate ¶
consolidate(
mentions: Iterable[EntityMention],
relations: Iterable[RelationMention] = (),
) -> TypeConsolidation
Consume the run's mentions (+ optional relations); return them remapped + the vocab.
Source code in packages/latence-core/src/latence_core/capability.py
Disambiguator ¶
Bases: Protocol
Links mentions to canonical entities + merges duplicates (CONTEXT Disambiguation).
The S8 Disambiguation Capability — the algorithmic crown. Disambiguation is a
corpus-level Stage: it consumes the whole run's
:class:~latence_core.contracts.EntityMentions (required — the mentions it resolves and
links) and, OPTIONALLY, the run's :class:~latence_core.contracts.RelationMentions (to
normalise onto the canonical entities), and emits
:class:~latence_core.contracts.DisambiguationRecords at two scopes:
- one
ENTITY-scope record per resolved :class:~latence_core.contracts.CanonicalEntity— a cross-document merged cluster with its member mentions, an audited, confidence-weighted merge log (no silent over-merge — a below-policy merge is logged not applied), an optional external-KB link (with a graceful fallback for unlinked mentions), and page-accurate Provenance + :class:~latence_core.contracts.Evidenceback to the source mentions; - one
RELATION-scope record per :class:~latence_core.contracts.NormalizedRelation— a relation whose endpoints are remapped to canonical entities and whose label is L2 normalised (fuzzy/alias, type-filtered, inverse-detected + direction-swapped).
Both carriers are supplied so relations can be normalised onto the entities resolved in the same pass; a Provider that only disambiguates entities ignores the relations. The mention side is REQUIRED (unlike Profiling's optional mentions) — Disambiguation with no mentions has nothing to resolve. Records are emitted deterministically (all ENTITY records in canonical-text order, then RELATION records), so a seeded run is byte-identical (Baseline bar). The CPU reference path must work with no embedder/KB; a FAISS/GPU blocking Provider is a pluggable option, not required (S8 AC).
chunks is a third, additive, defaulted carrier: the run's
:class:~latence_core.contracts.ChunkRecords, so a Provider that resolves mentions by
their surrounding context (the learned disambiguation.embedding Provider — the pod
proved a bare-surface embedding over-merges distinct short surfaces like IBM and
Apple, but surface+context separates them cleanly) can locate a mention's parent chunk
(by EntityMention.chunk_record_id) and read a bounded window of its content around
the mention's offset. It defaults to an empty tuple so the signature is backward-compatible:
the algorithmic cascade ignores it (its behaviour is unchanged), a mentions-only run passes
no chunks, and any existing Provider that only accepts (mentions, relations) still
satisfies the Protocol — the Runner threads chunks only when the parameter exists (W1
rework, #94).
disambiguate ¶
disambiguate(
mentions: Iterable[EntityMention],
relations: Iterable[RelationMention],
chunks: Iterable[ChunkRecord] = (),
) -> Iterator[DisambiguationRecord]
Consume the run's mentions (+ optional relations + optional chunks); yield records.
Source code in packages/latence-core/src/latence_core/capability.py
GraphAssembler ¶
Bases: Protocol
Builds the canonical knowledge graph from disambiguated records (CONTEXT Graph Assembly).
The S9 Graph Assembly Capability. Graph Assembly is a corpus-level Stage: it consumes
the whole run's :class:~latence_core.contracts.DisambiguationRecords (the canonical
entities and normalized relations the S8 Disambiguation Stage produced) and emits
:class:~latence_core.contracts.GraphRecords at two scopes:
- one
NODE-scope record per :class:~latence_core.contracts.CanonicalEntity— a graph node with a deterministic content-addressed id, its member mentions, source-document reach, an optional external-KB link, and :class:~latence_core.contracts.Evidenceback to the source mentions; - one
EDGE-scope record per :class:~latence_core.contracts.NormalizedRelation— a graph edge with a deterministic id, its head/tail node ids, the normalized label, and per-edge Evidence back to the source relation's mentions/documents (every edge carries Evidence, the S9 acceptance criterion).
An edge is emitted only when both its endpoints resolved to a node (endpoint validation — no dangling edges). Records are emitted deterministically (all NODE records in node-id order, then EDGE records in edge-id order), so a seeded run is byte-identical (Baseline bar) and two runs over the same corpus produce the same graph. The reference Provider is pure-Python, no embedder/KB required; a graph-DB-writer or embedding-augmented Provider is a pluggable option behind this same seam (ADR-0017: Export writes files, never a live DB).
assemble ¶
Consume the run's DisambiguationRecords; yield GraphRecords (nodes then edges).
GraphCompleter ¶
Bases: Protocol
Predicts missing KG edges over the assembled graph (CONTEXT Graph Assembly; W2-linkpred).
The post-Assembly link-prediction Capability (ADR-0037): an OPTIONAL corpus-level Stage
that consumes the whole run's assembled :class:~latence_core.contracts.GraphRecords (NODE +
EDGE) and yields ADDITIONAL EDGE-scope :class:~latence_core.contracts.GraphRecords for
predicted edges — the §19 predicted-vs-extracted split GraphAssembler carried as
is_predicted (ADR-0027) turned into its own Stage. It never re-emits the input nodes/edges
and never mutates an asserted edge; it only appends new predicted edges.
A predicted edge is a :class:~latence_core.contracts.GraphEdge at scope=EDGE,
UNMISTAKABLY marked properties["inferred"] = True (plus scorer/score/rank/
calibrated) so it is never silently merged into the asserted stream — the enterprise-honesty
crux (ADR-0037). Its Evidence is honest and model-derived, NOT fabricated mention offsets:
mention_ids = [] (there is no textual mention to cite), document_ids = the union of the
head + tail nodes' source_document_ids, snippet a human justification naming the
scorer + score, and confidence the calibrated score. A predicted edge that COLLIDES
(same endpoints +
label) with an asserted edge is DROPPED — it is already asserted — logged, not emitted.
Mirroring :class:GraphAssembler: corpus-level, Iterable in / Iterator out,
deterministic + seeded → byte-identical (predicted edges yielded in edge-id order), the CPU
reference (graph_completion.reference) pure-Python, no torch. A learned ULTRA/PyKEEN scorer
is a pluggable Provider behind this same seam (Slice 2). The Stage is genuinely OPTIONAL: a
stack with no graph_completion Stage behaves exactly as today.
complete ¶
Consume the assembled graph (NODE + EDGE); yield ADDITIONAL predicted EDGE records.
Never re-emits the input, never mutates asserted edges — only appends predicted
EDGE-scope :class:~latence_core.contracts.GraphRecords, each marked
properties["inferred"] = True with honest model-derived Evidence, in edge-id order
(deterministic). A predicted edge colliding with an asserted edge (same endpoints +
label) is dropped, not emitted.
Source code in packages/latence-core/src/latence_core/capability.py
ContextEnricher ¶
Bases: Protocol
Projects the assembled KG back onto each chunk as a compact header (W13 context enrichment).
The W13 context-enrichment Capability (ADR-0039): an OPTIONAL Stage placed AFTER Graph
Assembly that stamps each :class:~latence_core.contracts.ChunkRecord with a compact
:class:~latence_core.contracts.ContextHeader — its canonical entities + top-k KG-neighbor
triples — which the Export prepends to the EMBEDDING input ONLY. The connections a chunk needs
for retrieval relevance are ALREADY computed as the KG; this surfaces them into the vector + a
metadata column, without inflating the stored chunk text (the LLM window stays the clean chunk).
Shaped like :class:LabelInducer (a chunk→chunk transform that yields the SAME chunks with an
additive field populated via chunk.model_copy(update=...)) but corpus-level and multi-input,
like :class:Disambiguator: it consumes the run's :class:~latence_core.contracts.ChunkRecords
(what it enriches + passes through), :class:~latence_core.contracts.EntityMentions (a chunk's
canonical entities, via EntityMention.chunk_record_id) AND the assembled
:class:~latence_core.contracts.GraphRecords (NODE + EDGE — the entities' KG neighbors). It
yields the SAME chunks, 1:1 and order-stable, with context_header populated (or left
None
for a chunk with no gated canonical entity — nothing to add). Genuinely optional +
contract-preserving: offsets/provenance/content are untouched, so a stack without the Stage is
byte-identical (the field stays None and the Export prepends nothing).
Deterministic + seeded (the CPU reference context.kg_header is pure-Python, no model, no
key)
→ byte-identical: every collection in the header is sorted, and the neighbor triples are
confidence-sorted then CAPPED (max_neighbors_per_entity top-k per entity + an overall
max_triples) so a corpus-wide hub entity contributes only its TOP relations — the mandatory
anti-bloat + hub-entity guard (ADR-0039). The optional LLM situating-sentence mode is a
documented follow-on (deterministic=False), not the default.
enrich ¶
enrich(
chunks: Iterable[ChunkRecord],
mentions: Iterable[EntityMention],
graph_records: Iterable[GraphRecord],
) -> Iterator[ChunkRecord]
Consume the run's chunks + mentions + assembled graph; yield the SAME chunks enriched.
The chunk stream is threaded 1:1 and order-stable (never dropped or reordered): a chunk with
at least one confidence-gated canonical entity carries a populated
:class:~latence_core.contracts.ContextHeader; a chunk with none flows through with
context_header still None (byte-identical to no-stage for that chunk). Deterministic
+
seeded → byte-reproducible (sorted + capped + content-addressed, the Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
GraphFeatures
dataclass
¶
Per-node structural features over the assembled KG: centrality + community (ADR-0051).
The result a :class:GraphFeatureComputer returns: for every node in the assembled graph,
a centrality score and a community id. It is a value the Graph-Assembly Stage stamps
onto each :class:~latence_core.contracts.GraphNode's properties bag (centrality /
community) — the computer holds nothing and mutates nothing (ADR-0048). Invariants (enforced
at construction so a Provider cannot emit a half-populated result, so the downstream stamp is
total):
centralityandcommunitycover the same node-id set — every node gets both a centrality and a community, never one without the other (the anti-false-green contract: a computer that skips nodes fails here, not silently downstream);- each
centralityvalue is a finite float (the normalisation is the Provider's business — degree-centrality lands in[0, 1], PageRank sums to ~1 — but every value is a real number, never NaN/inf); - each
communityid is a non-empty string — the deterministic, content-addressed id of the node's community (the reference Providers use the community's lexicographically-smallestnode_id), so two runs over the same corpus agree byte-for-byte (the Baseline bar).
An empty graph yields the empty :class:GraphFeatures (both maps empty), never an error.
GraphFeatureComputer ¶
Bases: Protocol
Computes centrality + community per node over the assembled KG (CONTEXT Graph Assembly).
The index-time graph-feature enrichment Capability (ADR-0051): a pure transform that, given
the assembled graph's :class:~latence_core.contracts.GraphNodes and
:class:~latence_core.contracts.GraphEdges, returns one :class:GraphFeatures — a
centrality measure (degree / PageRank) and a community id per node. It holds no index,
runs no search (ADR-0048), names no model. The Graph-Assembly Stage OPTIONALLY resolves a
computer from its config['graph_features'] (the same {"provider", "config"} composition
Export uses for its Embedder) and stamps the returned features onto each node's properties
bag, so they flow into graph-nodes.parquet columns and — via Context Enrichment — onto each
chunk's :class:~latence_core.contracts.ContextHeader (the two consumers ADR-0051 names:
graph-augmented retrieval and the Knapsack packer's centrality / cluster_ids inputs).
Genuinely optional: a Stage that names no computer emits the graph byte-identically to today.
Behaviour VARIES behind the seam, so it ships two in-core reference adapters
(:mod:latence_core.stages.graph_features): graph_features.degree (degree centrality +
connected-components community) and graph_features.pagerank (PageRank centrality +
label-propagation community). Both are pure-Python, zero-dependency and DETERMINISTIC (fixed
iteration order, rounded scores, content-addressed community ids), so a seeded run is
byte-reproducible. They are references, not the blessed Louvain/Leiden community detection
(ADR-0051), which — like SPLADE/MUVERA — ships as a pluggable, license-verified package.
compute_features ¶
Compute one :class:GraphFeatures (centrality + community per node) over the KG.
Consumes the whole assembled graph (all nodes + all edges); returns a
:class:GraphFeatures covering every input node (never a partial map). An edge whose
endpoint is not among nodes is ignored (belt-and-braces — the assembler validates
endpoints). An empty graph yields the empty :class:GraphFeatures. Deterministic → a seeded
run is byte-reproducible (the Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
DeltaOutcome
dataclass
¶
DeltaOutcome(
records: list[Record] = list(),
retracted: list[Record] = list(),
entities_created: int = 0,
entities_merged: int = 0,
entities_split: int = 0,
edges_added: int = 0,
edges_retracted: int = 0,
affected_set_size: int = 0,
corpus_record_count: int = 0,
)
The result of applying a :class:Delta to the parent corpus (S11 affected-set recompute).
A :class:DeltaProcessor takes the last committed Corpus Version's corpus-level derived
records, the current run's freshly-extracted corpus-level records, and the classified
:class:~latence_core.contracts.Delta, and returns:
records— the new live derived set for Version N+1: the parent records the delta did not touch, spliced with the affected-set recompute's freshly-resolved records (the merge-on-add / split-on-delete result). This is what the Corpus Version commits.retracted— the derived records tombstoned by this delta (soft-retained on a Retraction, discarded on a Purge), so the audit knows exactly what left the live set.entities_created/entities_merged/entities_split— the affected-set recompute's effect on canonical entities vs the parent (a bridging doc that MERGED two clusters, a deleted doc that SPLIT one) — the S11 churn the Quality Report reports.edges_added/edges_retracted— the graph patch counts.affected_set_size/corpus_record_count— the perf-baseline witness: how many corpus-level records the affected-set recompute actually re-resolved (affected_set_size) out of the whole current corpus-level set (corpus_record_count). On an incremental (non-reconciliation) deltaaffected_set_sizeis the blocking-neighborhood closure the recompute touched, which is<= corpus_record_count— the measurable "recompute only the affected set" bound (ADR-0018). On a full Reconciliation the two are equal (the whole corpus is the affected region).
DeltaProcessor ¶
Bases: Protocol
Applies a Delta to the parent corpus with affected-set recompute (CONTEXT Delta).
The S11 Delta Capability (ADR-0018). A DeltaProcessor is model-agnostic and pure over
records: given the parent Corpus Version's corpus-level derived records
(:class:~latence_core.contracts.DisambiguationRecords +
:class:~latence_core.contracts.GraphRecords), the current run's freshly-extracted
corpus-level records, and the classified :class:~latence_core.contracts.Delta, it returns
a :class:DeltaOutcome: the new live derived set for Version N+1 (parent records the delta
did not touch, spliced with the affected-set recompute), the retracted set, and the
entity/edge churn counts.
The DeltaProcessor owns the corpus-level recompute of the committed record set — the
blocking-neighborhood affected set for Disambiguation and the graph patch by
source_document_ids (ports graph/patcher). (Doc-level Stages, Parse … Redaction, run
only on the NEW/CHANGED documents since W17/ADR-0043 — an unchanged document's records are
reused across runs and the processor consumes the union of reused + freshly-extracted
corpus-level records exactly as before, so the seam here is unchanged.) When the Delta signals
a full Reconciliation (drift crossed the threshold), the processor re-resolves ALL blocks
rather than only the affected set; the seam is identical, only the affected set differs. The
reference Provider is pure-Python, deterministic and CPU-viable (the blocking-neighborhood
:func:~latence_core.state.incremental.compute_affected_set closure + GraphPatcher); a
distributed blocking Provider is a pluggable upgrade behind this same seam.
apply_delta ¶
apply_delta(
parent_records: Iterable[Record],
current_records: Iterable[Record],
delta: Delta,
) -> DeltaOutcome
Apply delta to the parent corpus; return the patched live set + churn.
Source code in packages/latence-core/src/latence_core/capability.py
Embedder ¶
Bases: Protocol
Optionally embeds RAG-corpus text into vectors (CONTEXT — an opt-in Export augmentation).
The S9 opt-in Embedder Capability (ADR-0017: "embeddings are optional (an opt-in Embedder Provider) so any vector DB can ingest it"). It is not required for Export — the RAG-ready corpus exports its cleaned/chunked/PII-handled text with full Provenance/Classification whether or not an embedder is wired. When a Pipeline opts in, an Embedder maps each text to a fixed-width vector so the exported corpus carries embeddings a vector DB can ingest directly.
The reference embedder in core is a deterministic, dependency-free hashing embedder (no model, no weights, no license to verify — ADR-0012/0016), so the seam is exercised end to end offline; the default learned Embedder (IBM Granite Embedding r2, Apache-2.0; ADR-0045) is a pluggable Provider package, not a core dependency. The Capability names no model — only the seam.
embed ¶
SparseVector
dataclass
¶
A sparse term-weight vector — the index-time SPLADE-family signal (ADR-0050/0053).
The shape every vector DB's sparse index expects: two parallel columns, indices (the
term ids that have non-zero weight) and values (their weights), materialised at Export as
sparse_indices: list<int> + sparse_values: list<float> (ADR-0050). Invariants (enforced
at construction so a Provider cannot emit a malformed vector, and so the emission is
byte-deterministic regardless of a Provider's internal dict ordering):
indicesandvaluesare the same length;indicesare strictly ascending (hence unique) and non-negative — the canonical order a sparse index ingests, and the order two runs must agree on for a byte-identical export;- a zero vector (empty text, no surviving terms) is the empty
SparseVector()— never a dense run of zeros.
A :class:SparseEmbedder Provider builds one via :meth:from_terms from its natural
{term_id: weight} map, which sorts and validates in one place.
from_terms
classmethod
¶
Build a canonical :class:SparseVector from a {term_id: weight} map.
Sorts the terms by id (the ascending-index invariant) so the emitted vector is byte-identical no matter what order the Provider accumulated the terms in — the single place ordering is imposed, so no Provider re-implements it. Zero-weight terms are dropped (a sparse vector carries only non-zero weights).
Source code in packages/latence-core/src/latence_core/capability.py
SparseEmbedder ¶
Bases: Protocol
Encodes corpus text into a sparse term-weight vector (a Signal generator; ADR-0053).
The index-time SPLADE-family sibling of :class:Embedder for the shape a dense vector cannot
express: a variable-support term-weight map over a fixed vocabulary, emitted at Export as the
parallel sparse_indices / sparse_values columns a store's sparse index ingests directly
(ADR-0050). Like every signal generator it is a pure transform that holds nothing (ADR-0048)
and names no model — the framework names the seam.
The blessed real Provider (ADR-0053) composes a multilingual SPLADE model's term-weights with
the pipeline's confidence-gated NER entity terms + selected metadata, over the redacted corpus
text by default; that composition + its license-verified model ship as a pluggable package
(latence-splade), NOT in core. The in-core sparse.hashing reference is a deterministic,
dependency-free hashing sparse encoder that exercises the seam + emission path offline (no
model, no weights, no license to verify — ADR-0012/0016), exactly as embedding.hashing does
for the dense seam. It is a reference, not SPLADE.
vocab_size
property
¶
The size of the term-index space: every emitted index lies in [0, vocab_size).
encode_sparse ¶
Map each text to a :class:SparseVector (canonical ascending indices), in input order.
One vector per input text, in order (like :meth:Embedder.embed). Empty/degenerate text
yields the empty SparseVector() (never a crash). A deterministic Provider is
byte-reproducible (the Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
SparseSignals
dataclass
¶
SparseSignals(
entities: tuple[tuple[str, float], ...] = (),
metadata: Mapping[str, str] = dict(),
)
The per-record structured signals a composing SparseEmbedder injects at index time.
ADR-0053's sparse vector is not plain SPLADE(text) but the structured composition
SPLADE(text) ⊕ boost·entity_terms ⊕ boost·metadata_terms. The text-only
:meth:SparseEmbedder.encode_sparse seam can only build the model leg — no structured signal
crosses it — so a :class:ComposingSparseEmbedder receives this parallel per-record carrier and
folds the chunk's own structured signals into the emitted vector. That is what makes the
index-time and query-time sparse spaces match (the ADR-0053 one-composition-two-clocks
guarantee): without it the query encoder injects boosted entity/metadata terms the corpus never
carried, and a hybrid search on an injected term matches zero documents. A record with no
structured signals is :data:EMPTY_SPARSE_SIGNALS and composes to exactly the model leg —
identical to the text-only path (so the additive/byte-identical guarantee is untouched when no
signals exist).
entities— the chunk's(surface, confidence)entity mentions offered to the composition. The composition confidence-gates them; the embedder's redaction gate additionally drops any surface that no longer occurs in the (redacted) corpus text, so a masked PII surface is never re-injected as a term (ADR-0053, REDACTION-BY-DEFAULT).metadata— the chunk's{field: value}map from which the embedder's operator-selectedmetadata_fieldsare injected (e.g. its classification category).
ComposingSparseEmbedder ¶
Bases: Protocol
A :class:SparseEmbedder that also composes structured per-record signals (ADR-0053).
The index-time seam for the ADR-0053 structured composition. A plain :class:SparseEmbedder
exposes only the text-only :meth:~SparseEmbedder.encode_sparse (the model leg); a composing
embedder additionally accepts, per record, the :class:SparseSignals (confidence-bearing entity
surfaces + selected metadata) and folds them into the emitted vector — so the corpus's sparse
space carries the SAME boosted entity/metadata terms the query-side encoder injects (the
one-composition-two-clocks guarantee). Export detects this Capability and passes each row's
signals; an embedder that does NOT implement it (e.g. the deterministic sparse.hashing
reference) is driven text-only, byte-identically to today.
An implementer is also a :class:SparseEmbedder (it keeps the text-only encode_sparse for
the no-signal path); this Protocol declares only the additional composing method so an
isinstance check cleanly discriminates the two.
encode_sparse_composed ¶
encode_sparse_composed(
texts: Iterable[str], signals: Sequence[SparseSignals]
) -> Iterator[SparseVector]
Map each (text, signals) pair to its composed :class:SparseVector, in input order.
signals is parallel to texts (one :class:SparseSignals per text, same length +
order). One vector per input, in order; a record whose signals are
:data:EMPTY_SPARSE_SIGNALS composes to exactly the model leg (identical to
:meth:SparseEmbedder.encode_sparse on that text — the additive guarantee).
Source code in packages/latence-core/src/latence_core/capability.py
MultiVectorEmbedder ¶
Bases: Protocol
Encodes text into a variable-length multi-vector — one vector per token (ADR-0050).
The index-time generator for the ColBERT-style late-interaction shape: a text becomes a
sequence of fixed-width token vectors (not one pooled vector). Raw multi-vectors are heavy and
have a different access pattern, so Export writes them to a sidecar multivectors.parquet
(list<list<double>> keyed by record_id), never into records.parquet (ADR-0050). They
also feed an :class:FdeConverter to produce the dense fde_embedding column.
Experimental and off by default (ADR-0050). The in-core multivector.hashing reference is a
deterministic, dependency-free per-token hashing encoder that exercises the seam offline; a real
ColBERT-family encoder is a pluggable package.
dimension
property
¶
The fixed width of every token vector in every returned multi-vector.
encode_multivector ¶
Map each text to a list of dimension-wide token vectors, in input order.
One multi-vector per input text, in order. Empty/degenerate text yields an empty list (no
tokens, never a crash). Every token vector has width :attr:dimension. A deterministic
Provider is byte-reproducible.
Source code in packages/latence-core/src/latence_core/capability.py
FdeConverter ¶
Bases: Protocol
Converts a multi-vector into one fixed-width dense vector (MUVERA FDE; ADR-0050).
A data-oblivious, training-free reduction that maps a variable-length multi-vector to a
single dimension-wide dense vector, so late-interaction quality can be approximated inside a
customer's ordinary dense index with no special store — emitted at Export as the
fde_embedding column (ADR-0050). Pure transform, holds nothing, names no model.
Experimental and off by default (ADR-0050): the production converter decision (canonical
MUVERA vs a regularizer-refined variant) is deferred, and the quality-blessed canonical MUVERA
reference is a pluggable Provider (fde.muvera). The in-core fde.reference is a
deterministic, dependency-free SimHash space-partition-and-sum converter that exercises the seam
+ emission path offline — a genuine data-oblivious FDE (NOT a passthrough that lies, the
anti-false-green bar), without claiming a MaxSim-approximation quality guarantee on any corpus.
dimension
property
¶
The fixed width of the produced dense vector, independent of the multi-vector length.
convert ¶
Map each multi-vector to one dimension-wide dense vector, in input order.
One dense vector per input multi-vector, in order. An empty multi-vector yields the
zero vector of width :attr:dimension. A token vector whose width does not match the
converter's declared input width is rejected with a typed error (the faithful contract a
real converter enforces — anti-false-green). A deterministic Provider is byte-reproducible.
Source code in packages/latence-core/src/latence_core/capability.py
Tokenizer ¶
Bases: Protocol
Splits text into lexical terms — the varying seam of the BM25 term-stats export (ADR-0048).
BM25 is a lexical signal, so its corpus statistics (document frequency, term frequency,
document length, average document length) are a pure function of one thing that genuinely
varies: how text is split into terms. This Capability is that seam. Everything else in the
:class:~latence_core.bm25.Bm25Accumulator — the df/tf fold, the Robertson idf, the ascending
canonical order — is fixed math; the tokenisation is the knob a corpus tunes. Like every
retrieval-tooling piece it is a pure transform that holds nothing (ADR-0048) and names no
model — the framework names the seam.
Two in-core reference adapters ship behind it (:mod:latence_core.bm25), and they are
genuinely different tokenisations (not a clone + a rename — the anti-false-green bar for a real
seam): tokenizer.regex folds to lowercase Unicode alphanumeric runs (punctuation and the
underscore are boundaries and are dropped, matching the dense/sparse hashing references — so
Müller and Таможенный are whole terms, though an unspaced script like Chinese yields one
term per whitespace run: the reference is Unicode-aware, not a word segmenter, and a corpus that
needs segmentation plugs it in HERE), while tokenizer.whitespace splits
only on whitespace and keeps punctuation attached to the token (so "foo," and "foo" are
distinct terms). A corpus that wants a store's exact analyzer plugs its own Tokenizer in behind
this same seam. Both references are deterministic, so the emitted BM25 artifact is
byte-reproducible (the Baseline bar).
tokenize ¶
Map each text to its list of terms, in input order (one token list per input text).
One token list per input text, in order (like :meth:Embedder.embed). The terms appear in
their order of occurrence in the text (a BM25 term frequency counts occurrences, so order is
immaterial to the statistic but the contract is occurrence-order, not sorted). Empty or
term-free text yields the empty list [] (never a crash). A deterministic Tokenizer is
byte-reproducible (the Baseline bar).
Source code in packages/latence-core/src/latence_core/capability.py
Export ¶
Bases: Protocol
Materializes AI-ready outputs (JSONL + Parquet in S1).
A corpus Export Provider may optionally attach embeddings (ADR-0017: an opt-in
Embedder augmentation). It opts in by naming an :class:Embedder Provider in its own
Stage config['embedder'] (a {"provider": ..., "config": {...}} block); the Provider
resolves that Embedder and threads each exported record's text through it, adding a vector
per row. A Provider that names no embedder exports its text unchanged — embeddings are never
required for Export. The seam is one signature so a non-embedding Export (the KG / demo-site
Providers) satisfies it identically.
export ¶
Write records to out_dir on storage; return the output URIs.
ProviderRegistry ¶
Discovers Providers from latence.providers entry points.
Discovery is lazy and cached: entry points are enumerated once, then each is loaded (imported) only when its name is first requested. This keeps the registry cheap even when many heavy Provider packages are installed.
Source code in packages/latence-core/src/latence_core/capability.py
load ¶
Load and return the Provider class registered under name.
Source code in packages/latence-core/src/latence_core/capability.py
profile ¶
Return the Provider's declared :class:ProviderProfile, or None if none (P3-F1).
Loads the Provider class (reading a profile requires the class object) and reads
whichever declaration form it uses — a profile classmethod or a PROFILE
ClassVar (:func:~latence_core.providers.profile.profile_of). A Provider that
declares no profile returns None; the registry never fabricates one
(ADR-0036 §2). The result is cached alongside _loaded so a second call is free,
with a cached None meaning "declares none" (not "not yet read").
Raises KeyError for an unknown name (same as :meth:load) and TypeError
for a malformed declaration (e.g. a profile classmethod that returns a non-
ProviderProfile) — a typo surfaces rather than reading as "no profile".
Source code in packages/latence-core/src/latence_core/capability.py
profiles ¶
Map every registered Provider name to its declared profile (or None) (P3-F1).
Iterates :meth:names and reads each Provider's profile. Reading a profile
requires loading the Provider (importing it), which is acceptable for tooling
and the bake-off — the lazy single-name :meth:profile path stays lazy; only this
map forces the loads. A Provider whose import raises (a missing heavy/optional
dep, ADR-0016) is skipped-and-flagged — its name is simply omitted from the map,
never crashing the whole sweep — so a partially-installed workspace still yields
the profiles of every importable Provider. A Provider that imports cleanly but
declares no profile is present with a None value (a declared absence, distinct
from an import failure's omission).
Source code in packages/latence-core/src/latence_core/capability.py
protocol_members ¶
The member names a runtime_checkable Protocol requires, without touching an instance.
__protocol_attrs__ is the authoritative set on 3.12+; on 3.11 (this project's floor) typing
does not expose it publicly, so we fall back to the Protocol's own public dir() — which for
every Capability here yields exactly its declared members.
Source code in packages/latence-core/src/latence_core/capability.py
satisfies_capability ¶
isinstance(obj, protocol) for a Capability Protocol — WITHOUT invoking any member.
Why this exists rather than plain isinstance: CPython implements a runtime_checkable
Protocol check with hasattr(instance, name) over the Protocol's members, and several
Capabilities declare a property (:attr:Embedder.dimension, :attr:SparseEmbedder.
vocab_size, :attr:MultiVectorEmbedder.dimension). hasattr therefore calls that property
— so an isinstance check performed at wiring time executes Provider work that ADR-0016
says is deferred to first use. Concretely (audit R6): resolving sparse.splade behind an
Export's sparse_embedder spec made isinstance read vocab_size, which loaded a
Hugging Face checkpoint from the network during config validation, and made the R5
"every committed environment-stack component constructs" guard silently skip the whole Export.
The check here is structurally identical (every Protocol member must be present) but resolves
each member against the provider's class first: a property, a method and a class attribute
are all present on the class as objects, and looking them up there never runs the descriptor's
getter. Only a member absent from the class falls back to the instance — that is a plain
__init__-assigned attribute, whose lookup is a dict hit, not Provider work.
A Provider is still rejected exactly as before when it does not carry a member, so the loud
ContractError at wiring time (ADR-0034) is unchanged.