Schema algebra
Six decidable operations over schemas — comparison, minimization, extraction.
Most schema tools check one document at a time. Omnist's algebra compares two whole schemas and proves the answer — decidable because a schema is a closed automaton, not a heuristic over examples.
Why compatible_with matters: you're about to ship a schema change. Will every document ever written under the old schema still validate against the new one? That's not a question you want a reviewer guessing at — it's exactly what a CI gate can prove before merging:
v1 = parse_schema('record R { "host": string }\nroot R')
v2 = parse_schema('record R { "host": string, "port" [0,1]: integer }\nroot R')
v1.compatible_with(v2) # can readers of v2 still read v1's documents?
v2.compatible_with(v1) # the reverse -- v1 has no 'port' field to requirev1.compatible_with(v2): True -- adding an optional field is backward compatible
v2.compatible_with(v1): False -- v1 can't produce the 'port' field v2 might requireWhy equivalent matters: two schemas can describe the exact same set of documents while looking structurally nothing alike — different record names, different nesting, the same shape underneath. That happens constantly: two teams independently modeling the same data, or a refactor that reorganizes records without changing what they accept. equivalent answers "did anything actually change?" without a human eyeballing a diff:
a = parse_schema('record Left { "x": string, "y": integer }\n'
'record Right { "x": string, "y": integer }\n'
'record Root { "a": Left, "b": Right }\nroot Root')
b = parse_schema('record Pair { "x": string, "y": integer }\n'
'record Root { "a": Pair, "b": Pair }\nroot Root')
a.equivalent(b)True -- Left and Right are two different names for the same shape;
a and b accept exactly the same documentsWhy normalize matters: the schema above (a) carries dead weight — Left and Right are two spellings of one record. That's easy to end up with after independent authoring, or after infer (next step) drafts one record per sample instead of noticing the repeat. normalize collapses it to the canonical minimal form — same documents accepted, fewer records to maintain:
a.normalize()record Left {
"x": string,
"y": integer,
}
record Root {
"a": Left,
"b": Left,
}
root RootWhy extract matters: a shared schema often has more fields than any one consumer needs — a microservice reading only host shouldn't have to carry port's validation rules too. extract(*labels) computes the minimal subschema recognizing only documents built from the kept labels, dropping anything the removal makes unreachable. Deleting a mandatory field is an error, not silently allowed:
v2.extract("host") # subschema with only "host" -- "port" droppedrecord R {
"host": string,
}
root RWhy is_empty/prune matter: a schema can accidentally describe no documents at all — a mandatory reference cycle with no base case, easy to introduce by typo in a large hand-written schema and very hard to spot by reading. is_empty() catches it before it ships; prune() strips never-emittable fields and unreachable records left over from other edits:
empty = parse_schema('record A { "x": B }\nrecord B { "y": A }\nroot A')
empty.is_empty()
empty.compatible_with(v1) # vacuous: an empty schema accepts no documentsis_empty: True
empty.compatible_with(v1): TrueWhy lint matters: a pre-flight check for a schema you're about to ship or one a tool generated — the same duplication normalize fixes, flagged first without changing anything, so a CI gate can require a human look at it:
from omnist import lint
dup = parse_schema('record A { "x": string }\nrecord B { "x": string }\n'
'record Root { "a": A, "b": B }\nroot Root')
for finding in lint(dup):
print(finding)LintFinding(code='lint.duplicate-record', severity='warning', location='A, B',
message="records 'B' are structurally identical to 'A'; merge them with `schema normalize`")