Data contracts¶
The single Pydantic schema every inter-Stage Record shares, serialized to both JSONL and
Parquet/Arrow (ADR-0006). Provenance and
Classification are the cross-cutting carriers every record holds.
contracts ¶
Versioned data contracts shared across every Stage boundary.
One schema, dual serialization (ADR-0006): these Pydantic v2 models are the
single canonical definition of inter-Stage data. Every :class:Record carries
both :class:Provenance (immutable source lineage) and :class:Classification
(descriptive content attributes); validation fails if either is missing, so a
record can never silently lose its source or attributes at a Stage boundary.
Provenance ¶
Bases: BaseModel
The immutable chain from a record back to its source.
Required fields identify the source document unambiguously; the optional offset fields locate a derived record within it (populated by Parse/Chunk).
Classification ¶
Bases: BaseModel
Descriptive attributes of a document's content.
Attached at ingest / after Parse and inherited by every downstream record.
Record ¶
Bases: BaseModel
Base inter-Stage record: always carries Provenance and Classification.
Both are required with no default, so constructing a record without them
raises pydantic.ValidationError — the enforced Stage-boundary check.
PageMapOrigin ¶
Bases: StrEnum
Where a :class:PageMap's boundaries came from — measurement or convention.
Before this existed, "this document really is one page" and "I have no idea how this
document is paginated" were the SAME value: a single span covering everything, page 1.
Anything downstream that cited a page therefore cited page 1 with full confidence for
an entire OCR'd corpus (the ADR-0060 defect). Recording the origin does not recover the
boundaries — nothing can, from an assembled blob — it makes their ABSENCE a value a
consumer can see, filter on, and refuse.
PARSER — the Parser segmented the source itself (a PDF reader walking pages, an OCR
Provider that ran per page). The boundaries are measured.
SIDECAR — the boundaries were supplied out-of-band, in a
:data:PAGE_MAP_SIDECAR_SUFFIX file, by whoever produced the markdown. Measured by the
producer and cross-checked against the decoded text on the way in, so as trustworthy as
PARSER — but distinguishable, because the trust rests on a different artifact.
ASSUMED_SINGLE_PAGE — there was NO page structure available: a .txt note, or
markdown whose pagination was lost upstream. The single span is a convention so that
offset→page resolution has one code path, not a claim that the document has one page. A
consumer that needs real page citation must treat this as unknown, not as page 1.
PageSpan ¶
Bases: BaseModel
One page's half-open [char_start, char_end) span in the assembled text.
PageMap ¶
Bases: BaseModel
The page-boundary map carried on a parsed :class:DocumentRecord.
pages are contiguous, non-overlapping, ordered spans covering the whole
assembled text; total_chars is the assembled length (the end of the last
page). A single-page document (plain text) has exactly one span — the map is
always present after a successful Parse, never optional, so downstream
offset→page resolution has one code path. The live resolver over this map is
:class:latence_core.pagemap.PageOffsetIndex.
origin says whether those boundaries were MEASURED or ASSUMED (v21/ADR-0060). One
span with origin=PARSER is a one-page document; one span with
origin=ASSUMED_SINGLE_PAGE is a document whose pagination is unknown. They used to be
indistinguishable, which is how an entire OCR'd corpus came to cite page 1.
from_page_texts
classmethod
¶
from_page_texts(
page_texts: list[str],
separator: str = "\n\n",
*,
origin: PageMapOrigin = PageMapOrigin.PARSER
) -> PageMapAssembly
Assemble per-page text into one markdown string plus its page map.
The Parser-side constructor: hand it the per-page decoded text in page
order and it returns both the concatenated markdown and the
:class:PageMap recording where each page landed — the two are built
together so the boundaries are exact by construction, not reverse-engineered.
A single-page (or zero-page, coerced to one empty page) document is the
plain-text case: one span covering everything. A caller in that position —
a Parser handed a page-less blob — passes
origin=PageMapOrigin.ASSUMED_SINGLE_PAGE so the record says so; a Parser
that really walked the source's pages keeps the PARSER default.
Source code in packages/latence-core/src/latence_core/contracts.py
from_sidecar_json
classmethod
¶
Read a :data:PAGE_MAP_SIDECAR_SUFFIX sidecar into a validated SIDECAR map.
The consumer half of the out-of-band handoff (ADR-0060). data is the sidecar file
exactly as it was read — its own origin, if it carries one, is IGNORED and
overwritten with :attr:PageMapOrigin.SIDECAR, because a file cannot vouch for how the
boundaries in it were obtained; what a consumer can honestly record is that they arrived
out of band.
text_length is the length of the DECODED document text the map is supposed to
describe, and the check that makes the sidecar trustworthy: a sidecar whose
total_chars disagrees is stale (the markdown was regenerated, re-encoded, or
hand-edited), and its spans would resolve real offsets to confidently wrong pages. That
raises :class:~latence_core.errors.PageMapSidecarError — which a Parser turns into a
PARSE_ERROR record — rather than returning a map that looks fine.
Every other malformation (unreadable bytes, invalid JSON, a non-object payload, spans that are not contiguous, a schema version from the future) raises the same error, so a caller has exactly one failure to handle.
Source code in packages/latence-core/src/latence_core/contracts.py
PageMapAssembly ¶
Bases: BaseModel
The assembled markdown plus its :class:PageMap, built together by Parse.
PageSlice ¶
Bases: BaseModel
The page spans overlapping ONE chunk — that chunk's own, self-describing page map.
A :class:PageMap covers a whole document and therefore starts at page 1 and at
character 0. A slice is a window onto one: exactly the :class:PageSpan s that
overlap [chunk.char_start, chunk.char_end), in the document's ORIGINAL character
coordinates and with their original page_number s. A chunk that sits inside one
page carries one span; a chunk straddling a page break carries two; the pathological
case (a chunk spanning many tiny pages) carries a handful. That is the whole of what a
chunk needs, because the only question asked of it is sub-chunk: which page does an
offset INSIDE this chunk fall on.
Why this exists (v19). The map used to be document-scoped state carried on ONE record of the chunk stream — the document's first chunk (v18) — and adopted by a resolver from whichever record it happened to see first. That made page resolution depend on two things that are not properties of the chunk being resolved: that the map-carrying record was observed FIRST (order dependence), and that it SURVIVED every filtering Stage (drop dependence). Both failure modes returned a plausible-looking WRONG page. A slice removes the dependency by removing the shared state: every chunk answers for itself, in any order, with any other chunk missing.
The payload win the v18 carriage existed for is kept: ~2 spans (~60 bytes) per chunk against the ~30 KB whole-document map a 519-page document would otherwise duplicate.
Coordinates are NOT rebased to the chunk. char_start/char_end stay in
document space so resolution arithmetic is identical to the document map's and the
provenance a record cites is the document's own (ADR-0021/0031).
for_span
classmethod
¶
The slice of page_map overlapping the half-open [char_start, char_end).
char_end is exclusive, so the last page included is the one holding the last
included character — a chunk ending exactly on a page boundary does NOT drag in the
page that starts there. A zero-length or inverted span still yields the one page
containing char_start, so a slice is never empty.
Source code in packages/latence-core/src/latence_core/contracts.py
Disposition ¶
Bases: StrEnum
A parsed record's outcome disposition.
PARSED is the healthy path. PARSE_ERROR marks a document the Parser
could not decode (a corrupt PDF, an unsupported/spoofed format): the record is
still emitted — carrying its Provenance and the failure reason — so the run
does not crash and the failure is auditable in the Quality Report, but it
carries no usable content and downstream Stages skip it.
DocumentRecord ¶
Bases: Record
A whole-document record: the parsed Parse output.
content is the assembled markdown text; media_type records how to
interpret it. page_map (present when disposition == PARSED) records
the source page boundaries so any downstream offset resolves back to its
original page. disposition/error capture a graceful parse failure — a
corrupt file yields a PARSE_ERROR record, never an exception that aborts
the run.
size_after_parse
property
¶
Assembled-markdown size in bytes — the Provenance 'size-after-parse' (S2).
A property of the RECORD, not of any Parser: it reads content and nothing else, so
every Parser that produced one of these answers it identically. It used to be six
byte-identical size_after_parse(record) helpers, one per Parser package, each
docstring claiming to "mirror" the others — six copies of the same one-liner is six
chances for the signal the Runner sums into ParseQuality.bytes_after_parse to stop
meaning the same thing across Providers. Those helpers survive as one-line delegations to
this property, because each package publishes its own in __all__.
UTF-8 because that is the encoding the Parse seam assembles markdown in; the count is of ENCODED BYTES, not characters, so a document of accented or CJK text reports the size a sink actually stores.
ParserInput ¶
Bases: BaseModel
A raw document handed to a Parser: source lineage plus undecoded content.
This is the input side of the Parse seam (ADR-0019). Source produces a
ParserInput per source document — Provenance and Classification stamped,
but content still the raw bytes (or already-decoded text) exactly as read
from Storage, NOT yet turned into markdown. The Parser owns decoding and the
turn into a :class:DocumentRecord, so the seam fits real parsers (which
consume PDF/image/office BYTES), not just the S1 plain-text passthrough.
config carries per-input Parser hints (e.g. a forced encoding or an OCR
language) so a Source or the Runner can steer a Parser without a new contract.
content bytes are serialized to JSON as base64 (ser_json_bytes /
val_json_bytes), not as a UTF-8 string. This is a serialization-durability
invariant, not a field change: a raw source document can be arbitrary binary
(a PDF, an office file, a ZIP archive with a deflate stream), and Pydantic's
default bytes-as-UTF-8 JSON encoding raises on any byte sequence that is not
valid UTF-8 — which would tear the Runner's checkpoint the moment a genuinely
binary file (e.g. a Screened ZIP) is threaded through the Source/Intake seam.
base64 round-trips any bytes losslessly (model_validate_json yields the
identical bytes), so checkpoint/resume survives the messy, dangerous corpus the
demo is built to ingest. The model fields are unchanged, so the inter-Stage
contract (and its schema_version) is unchanged — only the on-disk JSON
encoding of already-binary content is made lossless.
ScreeningCheckpoint ¶
Bases: StrEnum
Which of Screening's two checkpoints produced a finding (CONTEXT glossary).
INTAKE runs before Parse over the raw document (malware, zip bombs,
file-type spoofing, oversized/corrupt files). CONTENT runs after Chunk
over the retrieval-sized text (prompt injection, harmful content, sensitivity
escalation).
ScreeningVerdict ¶
Bases: StrEnum
What Screening decided about a record.
PASS — nothing found; the record proceeds untouched. FLAG — a risk was
found but the record still proceeds, carrying a :class:RiskMarker so a RAG
consumer can exclude it. QUARANTINE — the record is dangerous and is
removed from every downstream Stage and Export (retained and inspectable with
the reason, CONTEXT Quarantine).
RiskMarker ¶
Bases: BaseModel
A risk flag Content Screening attaches to a record so it propagates.
The marker survives into the corpus (it is a field on the record contract, not an out-of-band note) so a downstream RAG consumer can filter on it — the S3 acceptance criterion "a risk marker that survives into the corpus so RAG consumers can exclude it".
InducedLabels ¶
Bases: BaseModel
Per-document label schema the schema_induction Stage induced (CONTEXT
Classification).
W4 restores the "point it at a folder and it discovers its own schema" property: an OPTIONAL
:class:~latence_core.capability.LabelInducer Stage (a small OpenAI-compatible LLM with
structured JSON output) reads a document and induces the entity / relation / PII types
actually evidenced in it, then annotates every record it produced with this carrier so the
downstream extraction Stages can UNION these induced types onto their config label floor
(config_labels ∪ induced, deduped, order-stable). Like :class:RiskMarker, it is a
field on the record contract (not an out-of-band note), so it survives into the corpus and
a downstream Stage reads it off the record it already holds — the additive extractor seam.
Every list is already sanitized by the inducing Provider (control chars stripped, length
≤ 64, count ≤ 40, deduped, normalized) — an :class:InducedLabels is trusted, bounded data a
zero-shot extractor prompt can consume, never executed. model_id records which LLM induced
it (audit); confidences maps a label to the model's 0..1 score for it (auditable, optional
— a label with no score is simply absent from the map). A slot the model evidenced no types for
is an empty list, not a fabricated one.
v17 adds the fourth slot guard_types (guardrail categories), so every LEARNED label set a
downstream Provider prompts with — entity, relation, PII, guardrail — has a per-record slot.
Whether a slot's config labels are a droppable RELEVANCE floor or a non-subsettable UNIVERSAL
SAFETY floor is the consumer's decision, expressed at the
:func:latence_core.induced.effective_labels seam via
:class:~latence_core.induced.LabelFloor.
ScreeningFinding ¶
Bases: BaseModel
One Screening decision about one record, recorded for the Quality Report.
Emitted at either checkpoint. A QUARANTINE verdict names the removed
document; a FLAG carries the :class:RiskMarker that was propagated. The
finding is the audit trail: every Quarantine disposition and reason surfaces
in the Quality Report (S3 AC), and a flag records what was marked and why.
QuarantineRecord ¶
Bases: Record
A record Screening removed from the pipeline (CONTEXT Quarantine).
Excluded from every downstream Stage and Export, but retained and
inspectable: Provenance is intact, the offending checkpoint/detector/reason
are recorded, and the raw byte size is kept so the audit knows what was held
back. It never reaches Parse (intake) or the corpus (content). A
QuarantineRecord carries no usable content — the point of Quarantine
is that the data does not flow, only its audit trail.
OffsetSegment ¶
Bases: BaseModel
One contiguous run mapping stripped-content offsets to original offsets.
Chunk content is the markup-stripped text; markup stripping only ever
deletes characters (chunking.strip_markup), so a chunk's surviving
characters form contiguous runs of the ORIGINAL parsed markdown. Within a run
the map is a constant shift: original offset = doc_start + (local -
local_start). A new segment begins at each stripped gap, so the whole
stripped→original map is this short list of run breakpoints (typically a handful
per chunk), not a per-character table.
local_start is the offset into the chunk's content; doc_start is the
original-markdown offset of that same character. The first segment always starts
at local_start == 0.
OffsetMap ¶
Bases: BaseModel
Run-length map from a chunk's stripped content to original offsets.
Carried on a :class:ChunkRecord so a downstream Stage that finds a sub-chunk
offset (an entity mention's chunk-local match) can recover that offset's TRUE
position in the parent document's assembled markdown — not merely the lower bound
char_start + local_offset gives once markup was stripped before the mention
within its chunk (ADR-0022 corrected, ADR-0031). The live resolver over this map
is :class:latence_core.offsetmap.OffsetIndex.
segments are ordered by local_start (strictly increasing, first at 0);
length is len(content) — the exclusive upper bound of the local domain, so
a half-open local span's original end is resolvable. A markup-free chunk has
exactly one segment (0, char_start): the map is then the identity shift and a
mention resolves to char_start + local exactly as before.
ContextHeader ¶
Bases: BaseModel
The KG projected back onto a chunk for retrieval coherence (CONTEXT Context Enrichment).
W13 (ADR-0039) restores the connections a chunk needs for retrieval relevance — its canonical
entities and their top-k knowledge-graph neighbor triples — that the RAG export otherwise
discards by embedding each chunk in isolation. The OPTIONAL context_enrichment Stage builds
this compact header per chunk from the run's already-computed :class:EntityMention +
:class:GraphRecord streams and the Export prepends it to the EMBEDDING input only: the
coherence inflates the vector + a metadata column, never the stored chunk text (the LLM window
stays the clean chunk, masked_content/content byte-unchanged).
Like :class:RiskMarker / :class:InducedLabels it is a field on the record contract (not
an out-of-band note), so it survives into the corpus and a downstream retriever reads it off the
chunk it already holds. It is deliberately COMPACT (the bloat discipline, ADR-0039): compact
rendered triples not neighbor prose, a top-k neighbor + confidence cap (the hub-entity guard),
and
join keys not raw KG text (query-time KG expansion is the retriever's job — ADR-0017, files not
a
live DB; W13 only emits the keys).
entities— the chunk's canonical entity names (sorted, deduped), andentity_typesthe aligned entity type per name (same length + order asentities).entity_confidencesis the aligned per-entity 0..1 admission confidence (ADR-0053) — the value the enrichment gate admitted the entity at, carried so a downstream composing SparseEmbedder'sconfidence_thresholdgates weak NER at index time by the identical rule;[]for a header built without it.neighbor_triples— the chunk's top KG-neighbor relations renderedA —label→ B, confidence-sorted and CAPPED (max_neighbors_per_entityper entity,max_triplesoverall) — the anti-bloat + hub-entity guard: a corpus-wide hub entity contributes only its TOP relations, never all of them.kg_node_ids— the deterministic content-addressed node ids of the chunk's own entities (the join keys a downstream retriever hybridises/reranks on).neighbor_node_ids— the node ids of the neighbor entities the triples reach (the query-time KG-expansion keys), sorted + deduped.kg_node_centrality/kg_node_community— the OPTIONAL ADR-0051 graph-feature projection: each entity's index-time centrality score and community id, aligned 1:1 withkg_node_ids(same length + order), so a downstream ranker reads a chunk's own entities' structural importance/cluster off the header it already holds. Both are[]when thegraph_featuresenrichment is OFF (the nodes carry no centrality) — byte-identical to a run without it — and both full-length (aligned withkg_node_ids) when it is ON.situating_sentence— OPTIONAL LLM-generated Contextual-Retrieval-style sentence (deterministic=False, a documented follow-on, ADR-0039);Nonein the deterministic default reference build.model_idrecords the LLM when that mode is used (audit),Nonefor the deterministic build.
is_empty
property
¶
True if the header carries no entities and no triples (nothing to prepend).
ChunkRecord ¶
Bases: Record
One retrieval-sized piece of a parsed document (CONTEXT Chunk).
Chunk splits a :class:DocumentRecord's markdown into retrieval-sized pieces
while preserving offsets, page alignment, Provenance and Classification
losslessly. Every chunk carries:
char_start/char_endon its Provenance — the half-open span the chunk occupies in the parent document's assembled markdown, so any chunk resolves back to its exact source offsets (round-trip: chunk → offset → original page via :class:~latence_core.pagemap.PageOffsetIndex);page_start/page_endon its Provenance — the source page(s) the chunk spans, resolved from the parent's page map;- the parent's Classification, inherited unchanged;
risk_markers— any :class:RiskMarkerContent Screening attached, which survive into the corpus so RAG consumers can exclude the chunk.
document_record_id links the chunk back to its parent document; index
is the chunk's ordinal within that document (0-based, contiguous).
page_slice carries this chunk's own page spans so a downstream Stage
that finds a sub-chunk offset (an entity mention) can resolve that offset's
own source page — not merely inherit the chunk's whole
[page_start, page_end] range. Without it, a mention on a later page of a
multi-page chunk would cite the chunk's first page.
Since v19 the chunk is SELF-DESCRIBING (:class:PageSlice): it holds exactly
the document page spans overlapping its own [char_start, char_end) — one
for a chunk inside a page, two for a chunk straddling a page break — in the
document's original coordinates. v18 instead carried the whole document map on
the document's FIRST chunk and had every Stage adopt it from the stream, which
made resolution depend on that one record being seen first and surviving every
filtering Stage; when it was not, resolution silently degraded to the chunk's
inherited page range — a plausible-looking WRONG page. A slice cannot express
that failure: there is no shared state and no ordering to depend on.
Resolve through :class:~latence_core.page_index.PageIndexResolver rather than
reading the field: the resolver owns the half-open end-of-span clamp and the
drift diagnostics. A None here means the producing Chunker supplied no page
spans at all — resolution raises
:class:~latence_core.errors.PageSliceMissingError rather than guessing
(ADR-0034).
offset_map carries the stripped-content→original-markdown offset map
(:class:OffsetMap) so a downstream Stage can recover a sub-chunk offset's TRUE
original position. content is markup-stripped, so char_start + local is
only a lower bound on a mention's real offset once markup was stripped before it;
the map corrects that (ADR-0031). Optional and additive — absent (pre-v11 chunk,
or no map), downstream degrades to the char_start + local shift.
EntityMention ¶
Bases: Record
One typed entity mention found in a chunk's text (CONTEXT Entity Extraction).
Entity Extraction finds typed entity mentions in chunked text (zero-shot NER):
every mention is an inter-Stage :class:Record — it carries Provenance and
Classification like any other — so it can be threaded through the DAG and
exported. A mention records:
label— the entity type it was tagged as. The label set is supplied per Pipeline (zero-shot); no label set is hardcoded in core.text— the exact surface string of the mention as it appears in the parent document's assembled markdown.confidence— the extractor's 0..1 score for the mention.char_start/char_endon its Provenance — the half-open span the mention occupies in the parent document's assembled markdown (not in the chunk-local text), so a mention resolves back to its exact source offsets and, through the parent's page map, to its original source page(s) (the S4 round-trip: mention → offset → original page, via :class:~latence_core.pagemap.PageOffsetIndex).page_start/page_endon its Provenance — the source page(s) the mention span falls on, resolved from the parent's page map.- the parent chunk's Classification, inherited unchanged.
chunk_record_id/document_record_id link the mention back to the chunk it
was found in and to the document that chunk came from; index is the
mention's ordinal within its chunk (0-based).
RelationMention ¶
Bases: Record
One typed relation between two entity mentions (CONTEXT Relation Extraction).
Relation Extraction finds typed relations between entity mentions within a
document. A relation is an inter-Stage :class:Record — it carries Provenance
and Classification like :class:EntityMention — so it threads through the DAG,
is checkpointed/resumed uniformly, and is exported to the corpus directly. A
relation records:
label— the relation type (e.g.works_for,located_in). The relation label set is supplied per Pipeline (zero-shot / prompt-driven); no label set is hardcoded in core, mirroring Entity Extraction.head_mention_id/tail_mention_id— therecord_idof the head (subject) and tail (object) :class:EntityMentionthe relation connects. The relation is directed head → tail.head_text/tail_text— the head/tail surface strings, carried denormalised so a consumer (and Graph Assembly's Evidence) reads the relation without re-joining to the mention set.confidence— the extractor's 0..1 score for the relation.char_start/char_endon its Provenance — the half-open span in the parent document's assembled markdown that covers both endpoints (from the earlier mention's start to the later mention's end), so the relation resolves back to source offsets and, through the parent's page map, to its source page(s). The page span is resolved the same way (mention → offset → page).- the parent document's Classification, inherited unchanged.
document_record_id links the relation back to the document its two mentions
belong to; index is the relation's ordinal within that document (0-based).
The head and tail must be distinct mentions (no self-relation), and the char span must be present and well-ordered — a relation with no span or an inverted span fails validation, so a Provider that forgets to resolve the endpoint offsets cannot silently emit an un-resolvable relation.
TypeKind ¶
Bases: StrEnum
Which label vocabulary a :class:CanonicalType belongs to.
Entity types and relation types are canonicalized in the SAME pass but never mixed: an
entity type organization and a relation type organization of are different
vocabularies, and folding one into the other would corrupt the graph. Every lookup is
therefore keyed by (kind, label).
CanonicalType ¶
Bases: BaseModel
One canonical type and every raw induced label folded into it (T3).
Per-chunk induction produces type drift — org here, organization there,
company in a third chunk — because each chunk induces its own labels. The
Type-Consolidation Stage clusters those synonymous surfaces and elects ONE canonical
label per cluster; this record is the cluster.
aliases holds every OTHER raw label in the cluster (sorted, canonical excluded), so the
alias→canonical map an operator reads on the Quality Report is exactly
{alias: canonical for alias in aliases}. strategies names the clustering rungs that
produced the folds (normalized/alias/acronym/prefix/head_noun/
embedding), so a suspicious merge is traceable to the rule that made it — the audit the
entity resolver's merge_decisions provides on the entity side. mention_count is how
many mentions/relations carried a member label, the salience signal an operator sorts by.
labels
property
¶
Every raw label this cluster covers — the canonical one plus its aliases.
TypeVocabulary ¶
Bases: BaseModel
The corpus's canonical type vocabulary + its alias map (T3) — persistable and mergeable.
The artifact the Type-Consolidation Stage emits: the M canonical types the run's N
raw induced types collapsed to, each with the aliases folded into it. It is a plain
serializable model (not a :class:Record) because it describes the corpus's SCHEMA, not a
span of a document — it has no Provenance to carry. The Runner persists it beside the
Stage's checkpoint, which is what makes it survive the run (ADR-0017: files are the source
of truth).
Mergeability is a first-class property (designed for the T4 incremental-delta lane): a
later run merges its freshly-induced vocabulary INTO the persisted one with
:meth:merge, which is deliberately append-don't-churn — an existing canonical label is
NEVER renamed by a later run (that would rewrite the type of every node already in the KG);
new aliases attach to the existing cluster and genuinely new types are appended.
of_kind ¶
The clusters of one kind, in emission order (canonical-label order).
alias_map ¶
{raw label: canonical label} for one kind — the operator-facing consolidation map.
Only genuine ALIASES appear (a type that was already canonical maps to itself and is omitted), so the map an operator reads on the Quality Report is exactly the set of rewrites the run performed — an empty map means nothing was consolidated.
Source code in packages/latence-core/src/latence_core/contracts.py
canonical_for ¶
The canonical label for a raw label; the label itself when it is unknown.
Unknown-is-identity keeps the remap TOTAL: a label the vocabulary never saw (a delta run's brand-new type, a hand-written relation label) passes through unchanged rather than being dropped or mapped to a wrong cluster.
Source code in packages/latence-core/src/latence_core/contracts.py
merge ¶
Fold other into this vocabulary — append-don't-churn (the T4 delta contract).
For every cluster in other: if any of its labels already belongs to a cluster of the
same kind here, that EXISTING cluster absorbs it — the existing canonical label is kept
(renaming it would silently retype every node already in the KG), its aliases are unioned
and strategies unioned. Otherwise the cluster is appended verbatim. When a cluster in
other bridges two existing clusters, the first match (in this vocabulary's order) wins
and the bridged labels join it — deterministic, and it never splits an existing cluster.
mention_count is the LATEST observation, not a running sum (T4). self is the
vocabulary persisted by an earlier run and other the one a later run built over the
whole CURRENT corpus, so the two count the same mentions: summing them would report a
multiple of the true corpus salience on the Quality Report, and would grow without bound
as deltas accumulate — the count would also differ between two applications of the same
delta, breaking idempotency. So a cluster other observed takes other's count
(several other clusters folding into ONE existing cluster do sum, because those are
disjoint observations within one run), and a cluster other never observed keeps the
count it was persisted with.
The result is ordered by (kind, canonical) and every alias list sorted, so merging
is associative in outcome and byte-stable: the same pair of vocabularies always merges
to the identical document.
Source code in packages/latence-core/src/latence_core/contracts.py
1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 | |
RedactionAction ¶
Bases: StrEnum
What a Redaction Provider did to a detected PII span (CONTEXT Redaction).
MASK replaces the span with a fixed mask token (e.g. [EMAIL]) — the
default, lossy, irreversible. REPLACE substitutes a deterministic surrogate
of the same type (a pseudonym) so the text stays readable while the real value
is gone. HASH substitutes a stable salted hash so equal PII values remain
joinable across the corpus without exposing the value. TAG leaves the surface
text intact but records the span (detect-only) — used when policy for a
Classification sensitivity is to flag PII for review rather than remove it.
PIISpan ¶
Bases: BaseModel
One detected PII span in a document, with the action taken (CONTEXT Redaction).
Since W16 (ADR-0042) a PIISpan is carried on the :class:ChunkRecord the chunk-level
Redactor produced: char_start/char_end are then offsets into that chunk's
content (the coordinate system its masked_content is masked in — chunk-LOCAL, not
document-global), while page_start/page_end still resolve to the ORIGINAL source
page through the chunk's offset_map+page_slice. On the DEPRECATED document-level
:class:RedactionRecord path the offsets are document-global (the historical shape below).
The structured, auditable record of a single redaction: what type of PII was
found (pii_type, from the Provider's configured type set — the Capability
names none), where it sat in the parent text
(char_start/char_end — chunk-local content offsets on a ChunkRecord, or the
document-offset coordinate system on the deprecated RedactionRecord — the coordinate every
other Stage uses, so the span resolves back through the parent's page map to its
source page), what was done (action), and the placeholder that replaced
it in the masked variant (empty for a TAG action, which leaves the text
intact). detector names the recogniser that fired (regex:email,
gliner-pii) so a hybrid Provider's rule vs model provenance is auditable.
The raw PII value is deliberately NOT stored — a PIISpan carries the offset, the type, and the placeholder, never the sensitive surface string, so the audit trail itself never becomes a PII leak (S6 AC: no raw PII in logs/records).
RedactionRecord ¶
Bases: Record
A per-document Redaction result — DEPRECATED, superseded by chunk-level redaction (W16).
.. deprecated:: W16 (ADR-0042)
Document-level Redaction fed the WHOLE parsed document into a fixed PII model window
(768 tokens for the gliner-family), so any PII past that point was SILENTLY truncated
and leaked unmasked into the corpus. Redaction is now a chunk→chunk transform — the
:class:~latence_core.capability.PIIDetector seam consumes :class:ChunkRecords and
yields the same chunks with :attr:ChunkRecord.masked_content +
:attr:ChunkRecord.pii_spans populated (each chunk ≤ the model window, so nothing is
truncated). This RedactionRecord
contract is RETAINED for the deprecated doc-level path (the provider redact_documents
secondary method) and for prior-version JSON compatibility, but the blessed stacks and the
Runner's redaction Stage now flow chunks.
A per-document Redaction result: the masked text variant + the PII spans (S6).
Redaction detects PII and produces masked/replaced variants of the text (CONTEXT
Redaction). Like every inter-Stage carrier it is a :class:Record, so it
carries Provenance and Classification and threads through the DAG / exports to the
corpus — the masked_content is the PII-handled RAG-corpus text an Export
materializes (ADR-0017). A RedactionRecord records:
masked_content— the document's assembled markdown with every non-TAGPII span replaced by its placeholder (aTAG-only run leaves the text identical to the source but still records the spans).pii_spans— the structured, auditable list of every detected span (type, document offset, action, placeholder, page) — the S6 "structured record of detected PII spans" requirement. The raw PII value is never stored on a span.policy— the redaction policy name that was applied (keyed off the document'sClassification.sensitivity, S6 AC), recorded so the run is auditable end to end.- Provenance
char_start/char_end— the covered document extent[0, len); the parent document's Classification, inherited unchanged.
document_record_id links the record back to the document it redacted;
media_type mirrors the source (text/markdown). A document with no detected
PII yields a RedactionRecord whose masked_content equals the source and whose
pii_spans is empty (never an error) — so the corpus always has one PII-handled
variant per document.
FeatureScope ¶
Bases: StrEnum
The scope a :class:FeatureRecord describes (CONTEXT Profiling).
Profiling is a corpus-level Stage, so it emits at two scopes. DOCUMENT is
one record per parsed document, carrying that document's per-document features
(density, readability, Zipf α, compression ratio, structure). CORPUS is
exactly one record per run, carrying the cross-document aggregate features
(entity frequency, co-occurrence, type consensus, source coverage). Both are
inter-Stage :class:Records — the split is a scope tag, not two contracts, so a
consumer reads one exported stream and filters by scope.
DocumentFeatures ¶
Bases: BaseModel
The fixed per-document statistical + quality feature set (S7).
Computed over one parsed :class:DocumentRecord's assembled markdown. This is the
consolidated, fixed feature set the S7 issue calls for (the private stack's 9
enrichers RECAST into a lean closed set — no dynamic registry), so the schema is
stable and a seeded run is byte-identical:
char_count/word_count/sentence_count— raw size measures.unique_word_count— distinct lowercased word types (the vocabulary size).type_token_ratio—unique_word_count / word_count(lexical diversity; 0.0 for an empty document).density— non-whitespace characters as a fraction of all characters (0..1): how information-dense the text is versus padding/layout whitespace.readability— a normalised 0..1 readability score (higher = easier), derived from average sentence length and average word length (a Flesch-style reading-ease mapped into[0, 1]), dependency-free and deterministic.zipf_alpha— the fitted Zipfian exponent α of the word-frequency distribution (a well-formed natural-language corpus sits near 1.0); 0.0 when there are too few distinct words to fit.compression_ratio—compressed_size / raw_sizeunder zlib (0..1-ish): a proxy for redundancy/repetitiveness (boilerplate compresses hard, so a low ratio flags a repetitive/templated document); 0.0 for empty text.avg_word_length/avg_sentence_length— the shape measures the readability score is built from, surfaced for downstream tuning.
EntityCoOccurrence ¶
Bases: BaseModel
One cross-document co-occurrence: an ordered entity-text pair + its count (S7).
The pair is stored in a canonical order (left <= right lexicographically) so
the same unordered pair is counted once regardless of the order the two mentions
appeared in a document. document_count is the number of distinct documents
in which both entities co-occur — the cross-document co-occurrence signal, not a
raw within-document tally.
CorpusFeatures ¶
Bases: BaseModel
The fixed cross-document statistical + quality feature set (S7).
Computed over the whole run — every parsed document and (when an Entity-Extraction
Stage is upstream) every :class:EntityMention. The consolidated cross-document
half of the RECAST enricher set:
document_count— parsed documents profiled.total_words/total_unique_words— corpus size + vocabulary.corpus_zipf_alpha— the Zipfian exponent fitted over the whole-corpus word-frequency distribution (the corpus-level Zipf α the issue names).language_coverage— per-Classification.languagedocument counts (source coverage by language).category_coverage/sensitivity_coverage— the same source-coverage breakdown by content category and sensitivity.entity_frequency— cross-document entity mention counts, keyed by the mention surface text (how often each entity appears corpus-wide).entity_type_frequency— mention counts keyed by entity label.entity_type_consensus— per entity text, the fraction of its mentions that carry its single most-common label (1.0 = every mention agrees on the type; a lower value flags an entity the extractor typed inconsistently across the corpus). Only entities seen at least twice are scored.co_occurrences— the top cross-document entity co-occurrence pairs (:class:EntityCoOccurrence), most-frequent first, capped for a bounded report.
Every map is emitted in a deterministic order (sorted), so a seeded run is byte-identical.
FeatureRecord ¶
Bases: Record
A corpus-level Profiling result: a typed FeatureSet at one scope (CONTEXT Profiling).
Profiling computes statistical and quality features over the corpus. A FeatureRecord
is an inter-Stage :class:Record — it carries Provenance and Classification like
every other carrier — so it threads through the DAG, is checkpointed/resumed
uniformly, and exports directly. Exactly one of document/corpus is populated,
selected by scope:
scope == DOCUMENT:documentholds the per-document :class:DocumentFeaturesanddocument_record_idlinks back to the profiled DocumentRecord; its Provenance is that document's lineage, its Classification inherited unchanged.corpusisNone.scope == CORPUS:corpusholds the cross-document :class:CorpusFeatures;document_record_idisNoneand its Provenance is the synthetic corpus-scope lineage (the whole run is its source).documentisNone.
The scope/payload consistency is contract-enforced, so a Provider cannot emit a document-scope record with corpus features (or vice-versa) or leave both empty.
Evidence ¶
Bases: BaseModel
The justification linking an inference back to its source mentions (CONTEXT Evidence).
Evidence is the record linking a knowledge-graph edge — or, in S8, a Disambiguation
merge/link decision — back to the specific source mentions and offsets that justify
it, with a confidence value. It is deliberately distinct from :class:Provenance:
Provenance is source lineage (which file, which page, which char span a record came
from); Evidence is justification for an inference (which mentions made us believe two
surface strings name the same entity, and how strongly). A merge that folds two
mentions into one canonical entity carries the Evidence for why — so no merge is a
silent, unexplained act.
mention_ids are the record_ids of the source :class:EntityMentions (or, for a
relation, the endpoints) the inference rests on; document_ids the distinct source
documents they came from (the cross-document reach of the evidence); snippet an
optional short human-readable justification (never raw sensitive content — a surface
string or the strategy name), and confidence the 0..1 strength of the justification.
MergeStrategy ¶
Bases: StrEnum
Which resolver strategy justified a merge/link decision (ported EntityResolver cascade).
The ported resolution/entity_resolver.py asset — taken as-is — resolves entity
mentions with a 5-strategy cascade, tried in order of descending precision so the
strongest available signal wins and the reason is auditable:
EXACT— identical normalised surface strings (case/whitespace-folded).ALIAS— a configured alias/synonym maps one surface to another's canonical.ACRONYM— an acronym matches the initials of a multi-word name (IBM↔International Business Machines).SUBSTRING— one name is a whole-word substring of the other (Acme↔Acme Corporation), gated by a length ratio soa⊄about.EMBEDDING— a vector-similarity match above a threshold (the CPU reference Provider has no embedder, so this strategy is inert there and is the pluggable FAISS/GPU upgrade path, S8 AC).
KB_LINK is the linking (not merge) disposition: the GLinker pipeline matched a
mention to a canonical entity in an external KB (L2→L3→L4→L0). Recorded as the
justification on a :class:CanonicalEntity that carries a kb_id.
MergeDecision ¶
Bases: BaseModel
One audited, confidence-weighted merge/link decision (no silent over-merge, S8 AC).
The ported EntityResolver keeps an audited merge log: every candidate merge —
applied or not — is recorded with the strategy that proposed it, its confidence, and
the Evidence, so a run never silently over-merges. A decision below the confidence
policy is recorded with applied == False and is NOT folded into a canonical
entity — the S8 acceptance criterion "merges below the confidence policy are not
applied and are logged".
source_text/target_text are the two surface strings the decision related
(source folded into target); strategy the cascade rung that proposed it;
confidence its 0..1 score; applied whether the confidence policy accepted it;
evidence the justification (the mentions/offsets). reason is a short
human-readable note (e.g. "acronym: IBM ~ International Business Machines").
CanonicalEntity ¶
Bases: Record
A canonical entity: a merged cross-document cluster of mentions (CONTEXT Disambiguation).
Disambiguation links entity mentions to canonical entities and merges duplicates across
documents. A CanonicalEntity is the entity-scope output: one per resolved cluster,
carrying the chosen canonical surface (canonical_text) and entity_type, the
member_mention_ids (the record_ids of every :class:EntityMention folded into
it), the source_document_ids it spans (its cross-document reach — the affected-set
key S11 keys incremental recompute off), the merge_decisions that built it (the
audit trail), and its :class:Evidence. When the cluster linked to an external KB, the
matched id is recorded in kb_id (None for an unlinked entity — the graceful
fallback, S8 AC).
Like every inter-Stage carrier it is a :class:Record, so it carries Provenance and
Classification and threads through the DAG / exports. Its Provenance is the canonical
mention's lineage — page-accurate via :class:~latence_core.pagemap.PageOffsetIndex, so
a downstream KG edge can cite "page 7 of the contract", the S8 page-map-integration
sub-task — and its Classification is inherited from that mention. mention_count is
the size of the cluster; a singleton entity (no duplicate found) is valid — it is a
canonical entity of one mention, not an error.
NormalizedRelation ¶
Bases: Record
A relation remapped onto canonical entities, label normalised (ported RelationNormalizer).
The ported relations/normalizer.py asset, taken as-is:
GLinker-style relation L2 resolution — fuzzy + alias label normalisation, type
filtering, and inverse detection + direction swap (an employed_by relation
whose configured inverse is employs is rewritten to the canonical direction). A
NormalizedRelation is the relation-scope Disambiguation output: it takes a source
:class:RelationMention and rewrites its endpoints from mention ids to the
head_entity_id/tail_entity_id of the :class:CanonicalEntitys they resolved to,
and its label to the normalised canonical relation label. inverted records
whether the head/tail were swapped to reach the canonical direction (so the rewrite is
auditable). source_relation_id links back to the original RelationMention.
Like every carrier it is a :class:Record (Provenance + Classification, inherited from
the source relation), so it threads through the DAG and exports. A relation whose head
or tail did not resolve to a canonical entity is dropped by the Provider (not emitted as
a broken relation) and counted in the Quality Report — never silently mis-linked.
DisambiguationScope ¶
Bases: StrEnum
The scope a :class:DisambiguationRecord describes (CONTEXT Disambiguation).
Disambiguation is a corpus-level Stage, so — like Profiling (ADR-0025) — it emits a
single scoped carrier rather than two contracts. ENTITY records carry one
:class:CanonicalEntity each (the entity-linking + merge half); RELATION records
carry one :class:NormalizedRelation each (the relation-normalisation half). A consumer
reads one exported stream and filters by scope; the DAG, the checkpoint, and Export
all treat one carrier.
DisambiguationRecord ¶
Bases: Record
A corpus-level Disambiguation result: one canonical entity OR one normalized relation.
The scoped inter-Stage carrier for the Disambiguation Stage, mirroring
:class:FeatureRecord's scope split (ADR-0025/0026). Exactly one of
entity/relation is populated, selected by scope:
scope == ENTITY:entityholds the :class:CanonicalEntity;relationisNone. Its Provenance is the canonical mention's page-accurate lineage and its Classification is inherited from that mention.scope == RELATION:relationholds the :class:NormalizedRelation;entityisNone. Its Provenance/Classification are the source relation's.
The scope/payload consistency is contract-enforced, so a Provider cannot emit an entity-scope record with a relation payload (or leave both empty). Records are emitted deterministically (all ENTITY records in canonical-text order, then all RELATION records) so a seeded run is byte-identical (Baseline bar).
GraphNode ¶
Bases: Record
One node in the canonical knowledge graph (CONTEXT Graph Assembly).
Graph Assembly builds the canonical knowledge graph — nodes, edges, and per-edge
Evidence — from disambiguated entities and relations. A GraphNode is the graph's
entity half: exactly one node per :class:CanonicalEntity, carrying a
deterministic node_id (content-addressed: a sha256 over the corpus id and the
canonical entity id, so the same entity always yields the same node id and two runs over
the same corpus produce the same graph — the S9 "deterministic IDs" acceptance
criterion), the canonical_name and label (entity type), the entity_id of the
source :class:CanonicalEntity, the member_mention_ids folded into it, its
source_document_ids (cross-document reach), an optional external kb_id link, and
the :class:Evidence back to the source mentions that justify it.
Like every inter-Stage carrier it is a :class:Record, so it carries Provenance
(inherited from the canonical entity — page-accurate via PageOffsetIndex) and
Classification. properties is an open bag of node attributes materialised into the KG
export (confidence, mention_count, document_count), kept JSON-serialisable so it round-
trips through Parquet/TTL/GraphML.
GraphEdge ¶
Bases: Record
One edge in the canonical knowledge graph, with per-edge Evidence (S9 Graph Assembly).
A GraphEdge is the graph's relation half: exactly one edge per
:class:NormalizedRelation, carrying a deterministic edge_id (content-addressed:
a sha256 over the corpus id and the source relation id), the source_node_id /
target_node_id it connects (the deterministic ids of the head/tail
:class:GraphNodes), the normalized relation label, its confidence, and its
:class:Evidence — the record linking the edge back to the specific source mentions and
offsets that justify it, with a confidence value (CONTEXT Evidence, reused verbatim
from S8 for per-edge justification: every edge carries Evidence, the S9 acceptance
criterion). relation_id back-references the source :class:NormalizedRelation.
Like every inter-Stage carrier it is a :class:Record (Provenance + Classification,
inherited from the source relation). The head and tail node must be distinct (no
self-loop). properties is an open bag of edge attributes materialised into the KG
export (inverted, source_document_ids), kept JSON-serialisable.
HyperedgeRelation ¶
Bases: BaseModel
One directed relation carried inside a :class:GraphHyperedge (CONTEXT Hyperedge).
The Hyperedge's relation signature is the set of these triples — (head_node_id, label,
tail_node_id) — and two Hyperedges are dedup-equivalent only when BOTH their entity sets
AND their relation signatures match (ADR-0057: identical entity sets with differing
signatures are distinct facts about the same group and are never merged).
relation_id back-references the source relation record for audit; confidence is the
extractor's score, one input of the Hyperedge's gamma.
signature
property
¶
The dedup signature triple: (head_node_id, label, tail_node_id).
HyperedgeSourceRef ¶
Bases: BaseModel
A non-primary source span folded into a Hyperedge on dedup-merge (CONTEXT Hyperedge).
On merge the highest-γ instance's tau_text and Provenance stay primary and every other
instance is appended here (ADR-0057 / spec 1.4), so a corpus-repeated fact keeps its full
audit trail while support counts the repetitions. Offsets are in the parent document's
original coordinates, like every Provenance span.
GraphHyperedge ¶
Bases: Record
One Hyperedge: the m-ary unit of retrieval (CONTEXT Hyperedge, ADR-0057).
Where a :class:GraphEdge connects exactly two nodes, a Hyperedge connects the FULL set of
canonical entities co-occurring in one minimal text span, together with the directed
relations extracted from that span — so a joint fact stays one selectable unit instead of
fragmenting into disconnected pairs. It is the atomic item the chain-of-evidence selector
packs into a token budget, which is why its selection inputs are precomputed on the record
(ADR-0056: the index is built for the solver):
tau_text— the verbatim span text, in the corpus redaction variant (ADR-0057): it never contains text the exported corpus would not contain. The span's Provenance stays in original coordinates for audit and Purge.token_cost— the knapsack weight, computed at build time by the namedtoken_counterand treated downstream as an approximate ranking weight whose budget guarantee comes from an exact query-time verification pass (ADR-0058). The counter identity is stored beside the value so a later mismatch is detectable, never silent.gamma— extraction confidence composed from link/relation/mention confidences (spec 1.5; uncalibrated until the calibration decision lands).support— how many distinct source spans asserted this same fact (dedup-merge counter); a frequency prior for scoring.has_relations—Falsemarks a weak Hyperedge (entities only), emitted behind a config toggle and utility-discounted downstream.
hyperedge_id is deterministic and content-addressed over the corpus id, the sorted
entity-node set, and the sorted relation signature — NOT over the span position — so the
same fact found twice hashes to the same id and dedup-merge is the identity operation the
S11 affected-set machinery can key off. Like every carrier it is a :class:Record
(Provenance = the primary span, original coordinates; Classification inherited), and it
carries :class:Evidence linking back to the member mentions.
GraphScope ¶
Bases: StrEnum
The scope a :class:GraphRecord describes (CONTEXT Graph Assembly).
Graph Assembly is a corpus-level Stage, so — like Disambiguation (ADR-0026) and
Profiling (ADR-0025) — it emits a single scoped carrier rather than two contracts.
NODE records carry one :class:GraphNode each (the graph's entity half); EDGE
records carry one :class:GraphEdge each (the graph's relation half); HYPEREDGE
records carry one :class:GraphHyperedge each (the m-ary retrieval unit, ADR-0057 —
emitted only when the Stage is configured for hyperedges, so a stack that never opts in
never sees the scope). A consumer reads one exported stream and filters by scope; the
DAG, the checkpoint, and the KG Export all treat one carrier.
GraphRecord ¶
Bases: Record
A corpus-level Graph-Assembly result: one graph node OR one graph edge.
The scoped inter-Stage carrier for the Graph Assembly Stage, mirroring
:class:DisambiguationRecord's scope split (ADR-0026). Exactly one of node/edge
is populated, selected by scope:
scope == NODE:nodeholds the :class:GraphNode;edge/hyperedgeareNone. Its Provenance/Classification are the source canonical entity's (page-accurate).scope == EDGE:edgeholds the :class:GraphEdge;node/hyperedgeareNone. Its Provenance/Classification are the source normalized relation's.scope == HYPEREDGE:hyperedgeholds the :class:GraphHyperedge(ADR-0057);node/edgeareNone. Its Provenance is the primary span's, in original coordinates.
The scope/payload consistency is contract-enforced, so a Provider cannot emit a node-scope record with an edge payload (or leave both empty). Records are emitted deterministically (all NODE records in node-id order, then all EDGE records in edge-id order, then any HYPEREDGE records in hyperedge-id order), so a seeded run is byte-identical (Baseline bar).
DeltaKind ¶
Bases: StrEnum
How a document changed since the last committed Corpus Version (CONTEXT Delta).
A :class:Delta is a content-hash diff of the Source manifest against the last committed
Corpus Version (ports delta_detector). Each document falls into exactly one class:
ADDED— a document id not present in the parent Version (new source).UPDATED— the same document id, a new content hash (the text or its derived extraction changed): doc-level Stages re-run on it, its old derived records are retracted.DELETED— a document id in the parent Version absent from the new manifest: its records are retracted (or purged), affected clusters re-resolved.UNCHANGED— same id, same hash: nothing re-runs; its committed records are carried forward verbatim.
When is UPDATED reachable? (issue #42). UPDATED fires only when a document id
persists across two manifests with a changed content hash — i.e. the Source assigns a
stable identity independent of content. The reference source.local_folder
(:class:~latence_core.stages.source.LocalFolderSource) is content-addressed: a document's id
IS sha256 of its bytes, so any in-place edit mints a new id and retires the old one, and
the detector classifies that as ADDED (new id) + DELETED (old id) — never UPDATED.
So UPDATED is unreachable under the content-addressed local-folder Source and is exercised
only by a Source with content-independent stable ids (e.g. a path-keyed or database-keyed
Source). It is retained (not removed) because it is a valid, contract-versioned classification
the DeltaProcessor and DeltaRunner._commit already handle, and a stable-id Source is a
supported Source variant; removing it would be a lossy contract change.
DeletionMode ¶
Bases: StrEnum
The disposition for a deleted document (CONTEXT Retraction / Purge).
Deletion is dual-mode (ADR-0018). RETRACTION is the default soft tombstone: the
document and its derived records are excluded from every Export, the knowledge graph, and
the RAG corpus, but retained for audit and rollback, with affected clusters
re-resolved. PURGE is the hard GDPR / right-to-be-forgotten erasure: physical removal
of the source, all derived records, and their Evidence, followed by reconciliation of the
affected set — nothing derived survives.
DeltaEntry ¶
Bases: BaseModel
One document's classification within a :class:Delta (CONTEXT Delta).
The content-hash diff assigns each document a :class:DeltaKind. content_hash is the
deterministic SHA-256 the ported detector computed over the document's markdown plus its
derived entity texts and relation labels (None for a DELETED document, which no
longer has a current hash). file_name is carried for the audit; identity is the
document_id alone (never the file name).
Delta ¶
Bases: BaseModel
The classified change set since the last committed Corpus Version (CONTEXT Delta).
Computed as a content-hash diff of the Source manifest against the parent Version's
manifest (ports delta_detector): every document is an ADDED / UPDATED / DELETED /
UNCHANGED :class:DeltaEntry. parent_version is the Corpus Version this delta was
computed against (None for the very first, bootstrap delta against an empty corpus).
deletion_mode records how deleted documents are being handled (Retraction by default,
Purge on request). intentional_deletion records whether this delta's DELETED entries are
user-requested tombstones (an explicit :meth:retract/:meth:purge) rather than the
incidental delete-half of an ordinary in-place content edit: because document ids are
content-addressed, editing a file mints a new id and retires the old one, so the detector
classifies the old content-id DELETED even though the user retracted nothing. Only an
intentional_deletion DELETED id is a durable tombstone that stays excluded at Source on
every future run; an edit's incidental DELETED id must NOT be permanently blacklisted, or an
edit-then-revert to earlier content would be silently dropped. drift is the cheap drift
metric (fraction of the corpus changed), and triggered_reconciliation records whether it
crossed the threshold and forced a full Reconciliation — so the trigger and its outcome are
auditable (the S11 AC).
CorpusVersion ¶
Bases: BaseModel
An immutable, numbered committed state of the corpus (CONTEXT Corpus Version).
A Delta run reads Version N and transactionally produces Version N+1 (via the ported WAL +
atomic manifest swap); any prior Version is inspectable for audit and rollback. A
CorpusVersion is the manifest of that committed state: its version number, the
parent_version it succeeded (None for the initial Version 0), the :class:Delta
that produced it, the live record_count after the delta, a deterministic
fingerprint (a content hash over the committed record ids — identical input + config ⇒
identical fingerprint, the G1 determinism criterion), and reconciled (True when this
Version was produced by a full Reconciliation rather than an incremental affected-set
recompute). created_ts is excluded from the fingerprint so re-running a delta is
byte-identical apart from the timestamp.
DeltaChurn ¶
Bases: BaseModel
The delta run's churn roll-up for the Quality Report (S11 AC).
"Delta churn (docs ±, entities created/merged/split, edges ±, drift) lands in the Quality
Report" — the S11 acceptance criterion, kept to counts (no raw text), so it is safe to
persist. documents_added / documents_updated / documents_deleted classify the
delta; documents_unchanged are carried forward untouched. entities_created /
entities_merged / entities_split are the affected-set recompute's effect on
canonical entities (a bridging doc that MERGES two clusters, a deleted doc that SPLITS
one); edges_added / edges_retracted the graph patch. records_retracted /
records_purged count the soft-tombstoned vs physically-erased derived records.
canonical_types_added / type_aliases_added / canonical_types_total are the T4
type-vocabulary churn: how the delta EXTENDED the persisted canonical type vocabulary (new
canonical entries vs raw labels that aliased into an existing one).
drift is the metric and reconciliation_triggered whether it forced a full recompute,
with deletion_mode recording how deletes were handled. from_version / to_version
bracket the transaction.
affected_set_size / corpus_record_count are the perf-baseline witness that the
recompute was affected-set-scoped, not O(corpus): on an incremental (non-reconciliation)
delta affected_set_size (the corpus-level records the blocking-neighborhood recompute
actually re-resolved) is <= corpus_record_count (the whole current corpus-level set),
and a genuinely local change touches only a small fraction — the measurable ADR-0018
"recompute only the affected set" bound. On a full Reconciliation the two are equal.