ENGINE INTERNALS · THE REASONER

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 Rust

Formal 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.

THE ONE-LINE ANSWER

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:

Rust · oxd-core/ontology.rs
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.

THE FENCE IS A FEATURE

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.

TaskStatusSurfaced by
Classification: full transitive class hierarchyShippedsubsumptions
Coherence: unsatisfiable-class detection (TBox)Shippedunsatisfiable_classes() · is_coherent()
Consistency: disjoint-extent overlap (ABox)Shippedconsistent · violations
Realization: individual → classesNamed onlybuild_result · extents
Property inference: transitivity · inverses · chainsDeferredn/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:

EL normal form
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:

disjointness → conjunction-into-⊥
-- 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.

initial worklist
-- 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.

RuleMeaningFires
CR1transitivitya ⊑ b, b ⊑ c ⟹ a ⊑ c
CR2conjunctionX ⊑ a, X ⊑ b, a ⊓ b ⊑ c ⟹ X ⊑ c
CR3existential filler propagationX ⊑ a, a ⊑ ∃r.b ⟹ X ⊑ ∃r.b
CR4existential absorptionX ⊑ ∃r.b, b ⊑ c, ∃r.c ⊑ d ⟹ X ⊑ d
CR5⊥ through existentialsX ⊑ ∃r.b, b ⊑ ⊥ ⟹ X ⊑ ⊥
ONE RULE, TWO JOBS

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.

ComplexityEach fact processed once, fires only indexed rules: O(|output| × fan-out), not naïve O(n⁴)
Correctness anchorA full-fixpoint classify_naive kept as a test oracle
Checked againstSubsumptions, extents, consistency, unsat-sets + randomized parity tests

Step 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:

realization (ABox)
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.

VerdictMeansSurfaced by
Incoherent (TBox)some class is unsatisfiable: necessarily emptyunsatisfiable_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.

IT REPORTS, IT DOESN'T REJECT

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:

POST /classify

Explicit, on-demand classification of the whole ontology.

Lazy on-query

OxQL IS-A queries classify-on-read to resolve inferred types.

MVCC snapshots

Classification runs per-snapshot, so readers see a consistent closure.

OAG gate

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.

A FREE PROPERTY

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.

TIER 1Principled exclusions

∀, ⊔, ¬, cardinality, inverses, nominals: outside EL by definition, fenced off to keep classification in PTIME. Not “unfinished”; excluded on purpose.

TIER 2Deliberate deferrals

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.

TIER 3The one true gap

ABox realization into existentially-defined classes. The canonical case (OwnedThing ≡ ∃hasOwner.Person with rex hasOwner alicerex : 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.