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 |
| 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 |
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 |
| 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.
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.14 has no TypeForm, so the runtime input and output type-form
parameters use object annotations. Generic callback relationships still
express InputT -> InternalT and InternalT -> OutputT; runtime resolution
validates the actual type forms. Moving to Python 3.15 TypeForm can strengthen
those two annotations without changing declaration vocabulary or 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.
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"), MinLength(1)].
Resolution owns the exact string contract, effective lower bound, canonical
Python name, and external alias. From there:
- generated construction validates the exact string and length under the Python name;
- Mapping/JSON input reads
displayNameand reports that external path; - serialization projects
displayNameunless alias output is disabled; - JSON Schema uses
displayNameandminLength: 1; - introspection reports both names and the normalized constraint;
- derived partial Specs retain the same field truth while changing presence.
No 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.