The deterministic skeleton under the fuzz
Oxid-DB ships one reasoner: a hand-written, sound OWL 2 EL⊥ classifier. It is the exact, checkable logic that decides what actually follows from your ontology, and flags what contradicts it, underneath everything the model proposes.
Research preview · in-process · written in RustFormal methods underneath
Open the Russian dolls under a modern AI coding tool and you eventually hit Tree-sitter: incremental, error-tolerant parsers built on Bernard Lang's 1974 Generalized LR algorithm and a formal grammar per language. The lesson is that the death of Chomsky's generative grammars under the axe of LLMs was announced prematurely: for code, the fuzzy model depends on old, exact, formal-language machinery to do the load-bearing work.
Oxid-DB is the same story one layer up. The LLM / Sensory side proposes axioms: fuzzy guesses like "is a chunk a subclass of, or part of, a document?" The EL⊥ classifier is the deterministic, sound skeleton underneath that decides what actually follows, and what contradicts. It is the deterministic logical check against the fuzzier extraction process, done in-process, in Rust, inside the database.
LLMs did not kill formal grammars: for code, they depend on them. Oxid-DB is that story one layer up: the model guesses, the reasoner decides.
One reasoner, one algorithm
In the taxonomy of ontology reasoners, Oxid-DB belongs with the ELK / CEL / Snorocket / jcel family, profile reasoners for the EL description logic, not the HermiT / Pellet family that reason over full OWL 2 DL. That is a deliberate engineering choice, not a shortcoming.
classify.rs is Oxid-DB's ELK: its own reasoner for the EL family, embedded in the database instead of shelled out to a Java process. There is exactly one algorithm, and every reasoning path in the engine runs through it.
EL⊥ is the logic. ELK is a reasoner for that logic. OWL 2 EL is the W3C profile built on the same family. Oxid-DB's classifier is its ELK.
Why EL, not full DL
HermiT and Pellet reason over OWL 2 DL: union (⊔), universal (∀), negation (¬), cardinality, nominals, inverses. That expressiveness costs a tableau search that is worst-case ExpTime / N2ExpTime. Oxid-DB instead restricts its class language to exactly five shapes:
ClassExpr =| Named -- Beer, Person, Disease …| Top -- ⊤ (everything)| Bottom -- ⊥ (the empty class)| And(A, B) -- A ⊓ B| Some(r, A) -- ∃r.A
That is the EL⊥ profile: conjunction (And), existential restriction (Some = ∃r.C), ⊤ and ⊥. No ⊔, ∀, ¬, cardinality or nominals. The payoff is the result Baader, Brandt & Lutz proved in 2005 and ELK exploits: classification is polynomial, computed by fixpoint rule-application instead of backtracking search.
Anything the OWL importer sees outside this profile is collapsed to the nearest EL shape: a deliberate expressiveness narrowing, not a parse failure. You always get a tractable, terminating answer.
What it computes
Mapping onto the standard reasoning tasks: three are fully shipped, one is partial, one is deferred. The engine is honest about which is which.
| Task | Status | Surfaced by |
|---|---|---|
| Classification: full transitive class hierarchy | Shipped | subsumptions |
| Coherence: unsatisfiable-class detection (TBox) | Shipped | unsatisfiable_classes() · is_coherent() |
| Consistency: disjoint-extent overlap (ABox) | Shipped | consistent · violations |
| Realization: individual → classes | Named only | build_result · extents |
| Property inference: transitivity · inverses · chains | Deferred | n/a |
The one Named only row is the single genuine gap, covered in The honest scope below.
Step A: normalize the TBox
The algorithm is normalize, then run completion rules to a fixpoint. First every axiom is rewritten into one of four canonical shapes the rules know how to consume:
And(a, b) ⊑ c -- conjunctiona ⊑ ∃r.b -- existential on the right∃r.b ⊑ c -- existential on the lefta ⊑ ⊥ -- explicit unsatisfiability
The elegant move is how disjointness disappears into conjunction. There is no dedicated disjointness machinery at all:
-- every declared-disjoint pair (A, B) is stored-- as ONE ordinary axiom:A ⊓ B ⊑ ⊥-- "these two classes can never overlap" now runs-- on the same machinery as any other conjunction,-- there is no separate disjointness engine.
Step B: seed the facts
A worklist is primed with the truths that hold before any inference runs. Everything after this point is derived from these seeds by the completion rules.
-- the worklist is seeded with axiomatic truthsC ⊑ C -- reflexivityC ⊑ ⊤ -- top… -- every asserted SubClassOf… -- every explicit C ⊑ ⊥… -- every existential right-hand side
Step C: run five completion rules
Each rule takes known subsumptions and derives new ones. The engine processes one derived fact a ⊑ b at a time and fires only the rules that fact can trigger, until nothing new appears.
| Rule | Meaning | Fires |
|---|---|---|
| CR1 | transitivity | a ⊑ b, b ⊑ c ⟹ a ⊑ c |
| CR2 | conjunction | X ⊑ a, X ⊑ b, a ⊓ b ⊑ c ⟹ X ⊑ c |
| CR3 | existential filler propagation | X ⊑ a, a ⊑ ∃r.b ⟹ X ⊑ ∃r.b |
| CR4 | existential absorption | X ⊑ ∃r.b, b ⊑ c, ∃r.c ⊑ d ⟹ X ⊑ d |
| CR5 | ⊥ through existentials | X ⊑ ∃r.b, b ⊑ ⊥ ⟹ X ⊑ ⊥ |
Because disjointness is stored as A ⊓ B ⊑ ⊥, CR2 is also the unsatisfiability detector: any class forced under both A and B derives ⊑ ⊥ for free. ⊥ then flows downward via CR1 (unsatisfiable subclasses) and across roles via CR5.
classify_naive kept as a test oracleStep D: realize individuals
Once the class hierarchy is closed, build_result walks each individual's asserted classes and, using the finished subsumption closure, drops it into the extent of every superclass:
Heineken : Pilsner-- using the finished subsumption closure, the-- individual drops into the extent of every-- superclass at once:Heineken ∈ extent(Pilsner)Heineken ∈ extent(PaleLager)Heineken ∈ extent(Beer)Heineken ∈ extent(Alcohol)Heineken ∈ extent(⊤)
Then, for every disjoint pair, it intersects the two extents. Any individual landing in both is an ABox consistency violation: e.g. Whiskers : Cat and Whiskers : Plant with Cat ⊓ Plant ⊑ ⊥ is reported, not silently dropped.
Two different “bad” verdicts
The engine is careful about a distinction that is easy to gloss: a broken schema and a broken dataset are not the same failure, and both are returned as data rather than thrown as errors.
| Verdict | Means | Surfaced by |
|---|---|---|
| Incoherent (TBox) | some class is unsatisfiable: necessarily empty | unsatisfiable_classes() · is_coherent() |
| Inconsistent (ABox) | some individual is forced into ⊥ | consistent · violations |
Incoherence ≠ inconsistency: a TBox with an unsatisfiable class still has a model, the class is simply empty. Both are computed; both come back as structured results.
Today POST /classify still answers 200 on a contradiction; the “refuse the write” gate is deliberately deferred. The OAG surface has since layered an opt-in EL⊥ commit gate on top of this same engine for callers who do want writes blocked.
Where it plugs in
There is exactly one algorithm, and every reasoning path in the engine routes to it:
Explicit, on-demand classification of the whole ontology.
OxQL IS-A queries classify-on-read to resolve inferred types.
Classification runs per-snapshot, so readers see a consistent closure.
The check_consistency tool + the opt-in EL⊥ commit gate.
The Sensory indexer leans on this directly: it asserts axioms (SubClassOf, chunk ClassAssertion) and calls /classify to materialize the transitive IS-A closure. The named-subclass reasoning is genuinely done live by the database.
EL is monotonic: deleting a class can never create a new entailment. So the ontology delete-gate is a no-op by the mathematics, not by omission. Realization is strictly an add / import-side capability.
The honest scope: three tiers
The boundaries fall into three buckets. Only the last is a real gap.
∀, ⊔, ¬, cardinality, inverses, nominals: outside EL by definition, fenced off to keep classification in PTIME. Not “unfinished”; excluded on purpose.
Property transitivity / role chains (r∘s ⊑ t), complete-consistency phase B, the reject-gate phase C, incremental classification: all inside EL, all spec'd, simply not built yet.
ABox realization into existentially-defined classes. The canonical case (OwnedThing ≡ ∃hasOwner.Person with rex hasOwner alice ⟹ rex : OwnedThing) is not derived. The existential rules (CR3/CR4) are keyed by class and seeded from TBox axioms; an individual's role-fillers never feed them. A full EL reasoner does this in PTIME, so it is genuine incompleteness: spec'd in reasoner-02, not yet built.