Skip to content

Kernel ↔ pack contract

The contract between the kernel and a pack is small on purpose. A pack interface designed against a single document type will be wrong somewhere, so the kernel keeps narrow expectations and gives packs wide latitude: when a second pack needs something, it is hoisted then — not before. Three pieces make up the whole extension surface: DocumentPack, GoldenRecord, and ContentCapability.

Principles

⑤ Extensible — this contract is the extension surface; a new pack is a new document type, no kernel fork. · ③ Locally capable — a pack declares its ContentMode here. · ② Provider-agnostic — a pack never names a provider; it states what content it needs.

DocumentPack

A runtime_checkable Protocol (core/pack.py). A pack owns its record model, templates, label vocabulary, and the record → render-context mapping; the kernel owns locale formatting, the Jinja environment, storage, run state, providers, and export.

Member Kind What it gives the kernel
name attr Stable id used in config (document.pack: invoice) and as the registry key.
template_root property → Path Directory of the pack's archetype templates.
labels property → LabelRegistry Printed vocabulary for every language the pack supports.
content_capability property → ContentCapability Where content comes from and whether it needs a key (below).
table_names property → tuple[str, …] The table names to_rows() will produce — declared up front so a sink can create/validate destinations before the first record.
build_context(record) method → dict Turns a record into a Jinja context. Must include locale, currency, labels — the kernel's formatting filters read those from the context.
archetype_for(record) method → str Template name (no extension) to render this record with.
header_fields(record) method → RunningHeader The per-page running header (primary text + localised page_label), so the PDF renderer needs no knowledge of any pack's record shape.
default_source(*, selection, max_line_items, catalogue) method → DocumentSource The pack's document source for a run (below).

default_source is where three things meet: an optional catalogue artifact URI to draw from (file:// / gs:// / s3://, credential-free to read); a selection the pack must honour or refuse (a slice named french that emits English is a wasted run — the pack raises UnsupportedConstraint rather than silently ignoring); and the pack's decision of whether that source is key-free (invoice returns a procedural sampler; a text-heavy pack returns an LLM-backed source behind the same run loop).

GoldenRecord

Every document type produces a record — the computed ground truth — and the document is a projection of it (core/record.py). The surface is deliberately tiny; packs carry whatever else they need.

@runtime_checkable
class GoldenRecord(Protocol):
    record_id: str
    run_id: str
    locale: Locale
    def to_rows(self) -> TableRows: ...   # {"invoices": [...], "line_items": [...]}

to_rows() is what makes the export path document-agnostic — an invoice yields invoices + line_items; a contract might yield contracts + clauses + parties, and the exporter never needs to know which. One rule: golden rows correspond 1:1 with what appears on the page (a tiered line prints a parent row plus a row per band, so it contributes all of them), or per-row precision/ recall wouldn't be comparable between the golden set and an extractor's output. Records are computed, never extracted: a record that fails to reconcile refuses to exist rather than reach a PDF.

ContentCapability — does this pack need a key?

The one thing that decides whether a document type generates on a laptop with no cloud account is where its prose comes from (core/content.py). A pack declares it:

class ContentMode(StrEnum):
    PROCEDURAL = "procedural"   # built-in + computed; offline, deterministic, no key
    LLM_BACKED = "llm_backed"   # generated by an LLM in a one-time offline catalogue step

class ContentCapability:
    mode: ContentMode
    notes: str = ""
    requires_api_key -> bool    # True iff LLM_BACKED
    local_first      -> bool    # True iff PROCEDURAL

The kernel reads it through capability_of(pack), which defaults to PROCEDURAL for a pack written before this contract — the kernel keeps narrow expectations. Infrastructure (storage, state, export) defaults to local for either mode; only content generation differs.

An LLM_BACKED pack additionally implements LlmContentBuilder (catalogue_items() → what to generate, ingest(results) → take the text back), kept separate from DocumentPack so a procedural pack never stubs it out. build_catalogue(pack, mix, …) drives that offline step (and raises for a procedural pack). Invoice is PROCEDURAL, so an invoice run is local-first.

How the kernel calls the pack during a run

The worker drives the pack through the source and the renderer — it never reaches into pack internals:

sequenceDiagram
    autonumber
    participant W as work_run / GenerationWorker
    participant S as DocumentSource
    participant R as renderer
    participant P as DocumentPack
    participant B as BlobStore + manifest
    W->>S: generate(run_id, index)
    S-->>W: GoldenRecord (computed ground truth)
    W->>R: render(record)
    R->>P: archetype_for(record)
    R->>P: build_context(record)
    Note over R,P: HTML via Jinja (template_root + context)
    R->>P: header_fields(record) · labels
    Note over R: PDF: Chromium print, then optional degrade / handwriting
    R-->>W: RenderedDocument (bytes)
    W->>B: put(document) · record.to_rows() → golden shards · unit-manifest part

default_source(...) supplies the DocumentSource; render() calls render_record(pack, record) = render_template(pack.template_root, pack.archetype_for(record), pack.build_context(record)), and the PDF path also reads pack.header_fields(record) and pack.labels. Everything the kernel needs of a document type flows through those hooks — which is exactly why a new pack is a new document type and not a kernel change.

See Execution flows for the full run/build/export sequences and Invoice pack → Overview for this contract made concrete.