Architecture

didactic is a thin Python layer over panproto: didactic owns the public API and the model authoring experience; and panproto owns the runtime representation, the lens and schema VCS implementations, and the protocol codecs.

What didactic adds

What panproto provides

How a Model becomes a Theory

class Foo(dx.Model):           [_meta.py]   [_theory.py]      [panproto]
    x: int             -->     FieldSpecs    spec dict     Theory object
    y: str
  1. The metaclass walks the class's annotations once at class creation. Each annotation goes through didactic.types._types.classify which returns a didactic.types._types.TypeTranslation with an encode/decode pair plus a sort name. The result is a FieldSpec cached on __field_specs__.
  2. On first access of Foo.__theory__, the bridge in didactic.theory._theory builds a panproto-shaped spec dict from the FieldSpecs. This is a pure-Python dict.
  3. The dict goes through panproto.create_theory. The result is the panproto.Theory cached on the class.

Steps 1 and 2 are cheap and deterministic. Step 3 invokes the panproto runtime; failures here usually point at a malformed spec dict, not at user code.

How a value travels

A field's value is held in encoded form (always str) on the underlying ModelStorage. Reading the field decodes through the field's TypeTranslation. JSON dumps go through one extra encoding pass for non-natively-JSON types (datetime to ISO 8601, Decimal to numeric string, etc.); validation reverses the same conversion.

Why frozen, why immutable

Lenses, fingerprints, the schema VCS, and panproto's content-addressed hash all assume that values are immutable. didactic propagates that property at the Python level: instances cannot have fields rewritten, the storage is private, and the public update path (Model.with_(...)) always produces a new instance.

The cost is that mutable in-place updates are not available. The benefit is that every operation downstream of a Model is a pure function.