base.py is the whole formal model expressed as a Pydantic class hierarchy — the point is that mypy + Pydantic do the enforcing, so there's essentially no hand-written validation code. Four layers:
TruthStatus and ExtractionMethod are closed Literal unions, so values can't drift into "inference"/"derived" variants. Provenance is a frozen sub-model requiring both source and extraction_method — that's what makes R10's all-or-nothing rule structural: you either have a complete record or None.
Instance # membership in V; frozen (R7); carries `id` (R9)
├── EntityInstance
└── BaseStatement(Instance, Generic[SubjectT, ObjectT])The two subclasses being disjoint siblings is the strict partition of T into entity and predicate types. BaseStatement inheriting Instance is E ⊆ V — nothing derives the edge set, it's just "the instances whose class is a BaseStatement subclass."
dom(p)/ran(p) are the type parameters: class WorksFor(BaseStatement[Person, Organization]) and you're done — mypy checks statically, Pydantic at construction. Multi-type domain = a Union argument. Both SubjectT and ObjectT are bound to Instance rather than EntityInstance, which is what permits higher-order predication (R8): a statement can sit in either slot.
_ensure_provenance_tuple(mode="before"): lets callers pass a bareProvenanceor alist, normalizes to tuple. Notebase.py:109-110— theisinstance(data, dict)guard is commented out, so a non-dict input to amodel_validatecall would blow up on.get._reject_empty_provenance(mode="after"):provenance=()is an error. Empty tuple would be a third state between grounded and ungrounded, which R10 forbids.
truth_status defaults to "hypothetical" — presence of a statement is not assertion.
Traits are properties of predicate types (R1), so they're mixin classes, not fields:
class Knows(BaseStatement[Person, Person], Symmetric): ...Symmetric, Transitive, Functional, InverseFunctional are bare markers under a common Trait base, so introspection is uniform issubclass(x, Trait). datalog.py reads these and compiles them into rules.
Inverse[PartnerT] is the generic one. get_inverse() digs the type argument out of __orig_bases__, and resolves a ForwardRef/string partner against the declaring module's globals — needed for mutual inverses like WorksFor/Employs where one can't name the other yet. It's declared one-way: get_inverse(ParentOf) is None unless ParentOf also declares it.
_validate_inverse_declaration runs from __init_subclass__, so declaring Inverse[P] whose domain/range isn't P's swap is a TypeError at import time, not a silent bug. The globals().get(...) dance at base.py:136 exists because subscripting BaseStatement[Any, Any] for the PartnerT bound makes Pydantic build generic submodels while the module is still loading — firing the hook before the validator function exists.
AnyStatement = InstanceOf[BaseStatement[Any, Any]]— for "range is any statement." The comment atbase.py:141-147explains why: a parametrizedBaseStatement[Any, Any]slot would make Pydantic rebuild the value as the base class, destroying its concrete type (τ).InstanceOfvalidates byisinstanceand keeps the object as-is. Cost: raw dicts get rejected, which is fine sinceserialize.pypasses real instances._domain_range()— reads type args from__pydantic_generic_metadata__rather thanget_args, because a parametrized Pydantic generic is a real submodel class, not a typing alias.