Architecture¶
Talea uses Single Owner Architecture: every important semantic truth has one canonical owner, while runtime and tooling domains consume or project it.
flowchart TD
A[Python annotation] --> B[Schema resolution]
B --> C[Canonical Schema]
C --> D[Spec or Contract declaration]
D --> E[Validation compiler]
D --> F[Input compiler]
D --> G[Serialization compiler]
D --> H[Error projection]
D --> I[JSON Schema and OpenAPI]
D --> J[Introspection]
Field names, types, aliases, constraints, metadata, requiredness, recursive identity, and discriminator truth are not reinterpreted by each branch.
Independent domains¶
| Domain | Owns |
|---|---|
| schema | immutable resolved annotation structure |
| declaration | effective Spec fields, inheritance, defaults, hooks, trust, presence |
| validation | strict success/failure execution |
| input | Mapping/JSON conversion and boundary aggregation |
| serialization | Python/JSON projection and codecs |
| errors | stable detail, safe snapshots, rendering, JSON projection |
| tagged unions | one discriminator dispatch map |
| recursive references | finite named back-edges and lazy publication |
| resources | operation-local input budgets |
| incremental Contract items | lazy indexes, failure decision, and stream counts |
| JSON Lines framing | record units, one-based lines, UTF-8/newlines, strict decode boundary, and transport bytes |
| standards projection | Draft 2020-12 and OpenAPI Schema Objects |
Canonical owner map¶
The 0.2 capabilities extend existing owners rather than creating parallel representations:
| Truth | Canonical owner | Consumers |
|---|---|---|
| dataclass stored fields and lifecycle classification | immutable DataclassSchema resolved by schema |
validation, input, serialization, introspection, standards projection |
| NamedTuple ordered slots, defaults, arity, declaration/specialization identity | immutable NamedTupleSchema resolved by schema |
strict validation, positional input/output, recursion, resource policy, introspection, standards projection |
ReadOnly, WriteOnly, aliases, and Sensitive |
normalized declaration metadata | derivation, input/output, errors, introspection, standards projection |
| derived source, retained/omitted fields, selection, partial state, mode, and explicit name | immutable declaration provenance | presence, patching, introspection, standards projection |
| nested include/exclude grammar and immutable selection tree | serialization selection owner validated against canonical schema | class-owned compiled output plans |
| selected-plan retention | each Spec declaration's output artifacts | that class only, bounded to 32 immutable plans |
| external-input budgets | operation-local ResourcePolicy state |
Mapping and JSON input compilers |
| incremental item and invalid-item budgets | immutable talea.contract.ItemPolicy plus iterator-local counters |
Contract.iter_validate and Contract.iter_python |
| JSONL record and transport-byte truth | immutable talea.jsonl.JsonlPolicy plus iterator-local framing state |
Contract.iter_jsonl |
| JSON representations | canonical JSON representation owner | JSON input, JSON output, and standards projection |
| public validation failures | ErrorCode, ErrorData, and ValidationError |
every validation and input execution target |
| represented domain values | one immutable RepresentationSchema association between internal, input, output, and callback identity |
strict validation, input/output compilation, standards projection, introspection, and nested selection |
Dataclass execution specializes from DataclassSchema; it does not retain a
second field map or rediscover dataclasses.fields(). Directional derivation
projects normal Spec declarations from one provenance record. Nested selection
copies caller input into an immutable tree before field access, compiles only
when a nested selector is used, and has no process-global cache.
NamedTuple ownership¶
An annotated typing.NamedTuple declaration resolves once into a frozen
NamedTupleSchema. Its ordered fields, defaults, required count, nominal type,
and generic or recursive identity are the only structural truth consumed by
generated execution and public tooling:
flowchart TD
A[typing.NamedTuple declaration] --> B[Annotation resolution]
B --> C[NamedTupleSchema]
C --> D[Strict exact-type validation]
C --> E[Positional Python and JSON input]
C --> F[Tuple and JSON-array output]
C --> G[JSON Schema and OpenAPI]
C --> H[Introspection]
No consumer rereads _fields, _field_defaults, or __annotations__ on a
warm path. Direct positional operations preserve the protocol's array shape;
there is no _asdict() or Mapping facade. Unlike a Spec or dataclass,
NamedTuple therefore has no object-shaped external names or lifecycle hooks.
Unlike TypedDict, it is nominal at strict Python boundaries. Nested selection
treats the whole record as a leaf so this owner does not expand the established
object-field selection grammar.
Representation ownership¶
The representation declaration is
Annotated[Internal, Representation(...)]. Resolution creates one
RepresentationSchema; execution does not scan metadata again and there is no
callback registry. Strict validation consumes only the internal schema. An
external Python or decoded-JSON value is processed by the declared input
schema, passed to the synchronous loader once, and the result is validated by
the internal schema before it can escape.
Callbacks are trusted application code: their own work is not budgeted or
rolled back. Talea-owned traversal before and after the callback shares the
same operation-local ResourcePolicy state, so a small input expanded into a
large structural result remains bounded. Opaque represented classes receive an
exact runtime-type check, not permanent trust in hidden state. Existing
supported internal schemas retain their normal structural and trust rules.
Ordinary unions keep Talea's canonical branch ordering. A represented branch
is attempted only when its declared input schema is structurally plausible;
its loader ValueError becomes a branch validation failure and may allow the
next branch to run, while other exceptions propagate. Consequently trusted
loader side effects can occur in more than one attempted branch, and every
attempted input/result traversal consumes the shared resource budget. A union
with multiple representation declarations for the same internal contract is
rejected because callback ordering would otherwise be indistinguishable.
Python 3.15 uses TypeForm for the input and output type expressions, so
generic callback relationships express both the declared external types and
InputT -> InternalT -> OutputT. Python 3.14 retains the same relationships
with an object fallback for the two type-form values. Runtime resolution
validates the declarations on both versions; TypeForm adds no schema node,
callback branch, or execution behavior.
Output validates current internal truth, invokes the directly bound dumper once,
validates the candidate against the declared output schema, and feeds that
schema's normal Python or JSON projector. Standards modes project the declared
input or output schema without executing callbacks. Callback-free
RepresentationInfo and structural nested selection are projections of the
same node, not additional declaration models. A missing direction fails
explicitly and never falls through to repr, __dict__, or internal-object
serialization. Representation is root-public because these owners now form
one complete boundary contract.
Incremental Contract ownership¶
A retained Contract supplies its compiled strict validator or lazy external-Python input artifact to one small item-consumption owner. That owner adds only zero-based indexing, fail-fast versus explicit continuation, and operation-local item counts:
flowchart TD
A[Retained Contract artifacts] --> B[Incremental item owner]
B --> C[Strict validation]
B --> D[External Python conversion]
E[JSONL framing owner] --> B
The item owner is not a second Contract, schema interpreter, batch
materializer, transport decoder, or stream framework. JSONL owns physical
records and safe pre-validation errors, reuses the ordinary strict JSON syntax
owner, and shares this owner's counters for every pulled/continued record.
JSON Schema, OpenAPI, serialization, and ContractInfo continue to describe
T; an execution iterator adds no structural truth.
JSON Lines ownership¶
flowchart TD
A[Text or bytes record] --> B[JSONL framing]
B --> C[Strict JSON decoder]
C --> D[Decoded Python value]
D --> E[Incremental item owner]
E --> F[Retained Contract JSON input]
JsonlPolicy owns raw per-record and total transport bytes. ItemPolicy owns
logical records and continued invalid records across framing and validation.
ResourcePolicy owns traversal and error aggregation inside each decoded
value. The JSONL path does not call Contract.from_json() per line, duplicate
JSON policy, double-charge raw bytes, or compile per record.
Why compile generated Python¶
Repeated validation should not reflect on annotations or interpret a generic schema tree. Talea emits specialized Python source once and compiles it into ordinary CPython bytecode. Compiler-owned templates provide syntax. Runtime classes, constants, patterns, callbacks, and error constructors enter through validated names and bound globals rather than source interpolation.
This approach makes generated execution inspectable with normal Python tools and keeps the runtime pure Python. It also creates a security obligation, addressed by name validation, non-interpolation rules, annotation-resolution controls, and injection tests. See Security architecture.
Extension rule¶
New validators, serializers, adapters, or tooling should consume the canonical schema and declaration. An extension that rereads annotations or duplicates type semantics creates a competing truth path and is incompatible with the architecture.
One field through every operation¶
Consider
display_name: Annotated[str, Alias("displayName", legacy=("name",)), MinLength(1)].
Resolution owns the exact string contract, effective lower bound, canonical
Python name, current external name, and accepted historical input names. From
there:
- generated construction validates the exact string and length under the Python name;
- Mapping/JSON input reads exactly one of
displayNameorname, rejects both asalias_conflict, and reports the current external path; - serialization projects
displayNameunless alias output is disabled; - JSON Schema/OpenAPI input projects
displayNameandnamewith the sameminLength: 1value contract plus one per-field exact-spelling constraint; output projectsdisplayNameonly; - introspection reports the Python, current, legacy, and complete accepted names plus the normalized constraint;
- derived partial Specs retain the same field truth while changing presence;
- a
DataclassFieldretains the equivalent projection before its Mapping/JSON emitter compiles; - a
TaggedUnionSchemaretains the one branch-compatible discriminator vocabulary needed for pre-branch direct dispatch.
The dataclass and tagged nodes are canonical projections, not independent Alias
interpreters. No runtime subsystem independently rereads Annotated. This prevents the class of
bugs where validation accepts one name while schemas publish another, or PATCH
forgets a constraint owned by the source.
Declaration and lazy operation lifecycle¶
Class creation validates field order, defaults, inheritance, hooks, aliases, metadata, generic state, and supported annotations. It publishes an immutable declaration only after that work succeeds. The strict constructor is generated for concrete Specs; other operations compile on first use and are retained by the declaration or Contract.
This is “compile once,” not “compile the entire library eagerly.” A Spec that never parses JSON does not pay to build JSON input. A Spec that never uses replacement has no replacement operation. Recursive and mutually dependent declarations publish through finite references and guarded lifecycle state so the first caller cannot observe a partly initialized graph.
Why execution paths stay separate¶
Trusted construction, strict arbitrary validation, external Mapping input, JSON decoding/conversion, Python output, and JSON output have overlapping schema truth but different jobs:
| Path | Work it uniquely owns |
|---|---|
| construction | keyword/default/factory lifecycle and atomic instance commit |
| strict validation | accept an already-correct Python value without conversion |
| external Mapping | structural conversion, nested Spec construction, policy, aggregation |
| JSON input | transport check, strict/default codec, JSON representations, conversion |
| Python output | detached Python representation and current-state validation |
| JSON output | JSON-specific representation followed by a codec |
Routing all six through one interpreted adapter would obscure trust and charge simple operations for features they do not use. Sharing canonical truth does not require sharing one runtime pipeline.
Immutability, trust, and mutable children¶
Spec field bindings are immutable, but a list or dictionary stored in a field remains an ordinary mutable Python object. Canonical schema classifies whether instances are permanently trustworthy. Nested immutable scalar-only Specs can take trusted composition paths; reachable mutable state is revalidated when it crosses a later validation, replacement, or serialization boundary.
This avoids deep-freezing the Python object ecosystem while refusing to assume that an earlier validation permanently protects a mutable child. It also makes the cost model explicit: mutable current state pays for current-state proof.
Failure ownership and security consequences¶
Success paths do not allocate rich error detail. When an operation fails, the error owner snapshots bounded representations, applies Sensitive redaction, retains stable facts, and renders human text lazily. External traversal shares one operation-local resource state across recursive references and tagged branches; state is not global and cannot leak between calls.
Generated-code safety follows the same ownership rule. Templates own syntax; classes, constants, regular expressions, callbacks, and user names are bound as runtime values after validation rather than interpolated into source. See the security model for the full threat boundary.
How to evaluate an extension¶
Before adding a new type, constraint, serializer, or framework adapter, ask:
- Which canonical node owns its structural truth?
- Which existing execution owners must consume it?
- Can unused declarations avoid runtime cost?
- What strict Python and JSON representations are promised?
- How do errors, Sensitive state, recursion, and resource accounting compose?
- Can schema and introspection project it honestly?
The architectural kill test is concrete: after the change, no second subsystem should be able to disagree about that feature's type, input, output, or schema meaning because no second subsystem owns a competing interpretation.