Skip to content

Instantly share code, notes, and snippets.

@wware
Last active August 18, 2026 16:23
Show Gist options
  • Select an option

  • Save wware/dc42df424e427d55e6a59ed7426ad934 to your computer and use it in GitHub Desktop.

Select an option

Save wware/dc42df424e427d55e6a59ed7426ad934 to your computer and use it in GitHub Desktop.

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:

1. Vocabulary + provenance (base.py:28-52)

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.

2. The sorts: V and its partition (base.py:55-73)

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.

3. The two bits of real logic in the class

  • _ensure_provenance_tuple (mode="before"): lets callers pass a bare Provenance or a list, normalizes to tuple. Note base.py:109-110 — the isinstance(data, dict) guard is commented out, so a non-dict input to a model_validate call 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.

4. Traits (base.py:173-233)

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.

Two supporting escape hatches

  • AnyStatement = InstanceOf[BaseStatement[Any, Any]] — for "range is any statement." The comment at base.py:141-147 explains why: a parametrized BaseStatement[Any, Any] slot would make Pydantic rebuild the value as the base class, destroying its concrete type (τ). InstanceOf validates by isinstance and keeps the object as-is. Cost: raw dicts get rejected, which is fine since serialize.py passes real instances.
  • _domain_range() — reads type args from __pydantic_generic_metadata__ rather than get_args, because a parametrized Pydantic generic is a real submodel class, not a typing alias.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment