Skip to content

Lint Rules

This page is the full Ontopoiesis lint rule catalogue. For how to run lint, choose rules, and wire it into CI, see the Lint guide.

Rule Families

Prefix Profile Count Description
E1xx Default (errors) 15 Contradiction checks
W1xx Default (warnings) 5 Low-noise universal warnings
P1xx editorial 8 Publication and editorial guidance
M1xx modeling_risk 8 Modeling-risk guidance
D1xx description_logic 5 OWL 2 DL strictness

Default Baseline

Rules are .cypher files. test_*.cypher rows fail the run. warn_*.cypher rows appear separately without failing the run.

Contradictions

These rules detect impossible axioms or direct semantic clashes without requiring a classifier.

E101 test_subclass_nothing

Named classes explicitly asserted under owl:Nothing.

// Classes with an explicit SubClassOf to owl:Nothing, asserting unsatisfiability
// directly in the axiom set without requiring a reasoner to derive it.
MATCH
  (ax:N {kind: 'SubClassOf'})-[:E {role: 'sub'}]->(c:N {kind: 'Class'}),
  (ax)-[:E {role: 'super'}]->(:N {iri: 'http://www.w3.org/2002/07/owl#Nothing'})
WHERE c.iri IS NOT NULL
  AND NOT c.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
RETURN c.iri AS iri
ORDER BY iri

E102 test_class_assertion_nothing

Individuals explicitly asserted as instances of owl:Nothing.

// Individuals asserted as instances of owl:Nothing via ClassAssertion.
// owl:Nothing has no instances by definition, so this is a direct contradiction.
MATCH
  (ax:N {kind: 'ClassAssertion'})-[:E {role: 'class'}]->(:N {iri: 'http://www.w3.org/2002/07/owl#Nothing'}),
  (ax)-[:E {role: 'individual'}]->(i:N {kind: 'NamedIndividual'})
WHERE i.iri IS NOT NULL
RETURN i.iri AS iri
ORDER BY iri

E103 test_disjoint_classes_shared_subclass

A named class directly asserted as a subclass of two disjoint named classes.

// A class that is a direct SubClassOf two classes declared DisjointClasses. The shared
// subclass must be unsatisfiable (equivalent to owl:Nothing): it simultaneously inherits
// from two disjoint parents. Detectable from the axiom structure alone — no classifier
// run required — making it a useful pre-reasoning consistency check.
MATCH (c:N)-[:D {relation: 'subclass_of'}]->(a:N),
      (c)-[:D {relation: 'subclass_of'}]->(b:N),
      (a)-[:D {relation: 'disjoint_class'}]->(b)
WHERE a.iri < b.iri
RETURN a.iri AS disjoint_a, b.iri AS disjoint_b, c.iri AS shared_subclass
ORDER BY disjoint_a, disjoint_b, shared_subclass

E104 test_disjoint_classes_shared_individual

A named individual directly asserted into two disjoint named classes.

// Individuals directly asserted into two disjoint named classes. This is a direct
// ABox contradiction that does not require a classifier to derive.
MATCH
  (dc:N {kind: 'DisjointClasses'})-[:E {role: 'operand'}]->(a:N {kind: 'Class'}),
  (dc)-[:E {role: 'operand'}]->(b:N {kind: 'Class'}),
  (ax1:N {kind: 'ClassAssertion'})-[:E {role: 'class'}]->(a),
  (ax1)-[:E {role: 'individual'}]->(i:N {kind: 'NamedIndividual'}),
  (ax2:N {kind: 'ClassAssertion'})-[:E {role: 'class'}]->(b),
  (ax2)-[:E {role: 'individual'}]->(i)
WHERE a.iri < b.iri
RETURN
  a.iri AS disjoint_a,
  b.iri AS disjoint_b,
  i.iri AS individual_iri
ORDER BY disjoint_a, disjoint_b, individual_iri

E105 test_disjoint_equivalent_classes

Two named classes that are asserted both equivalent and disjoint.

// Class pairs that appear together in both an EquivalentClasses and a DisjointClasses
// axiom. This directly implies both classes are unsatisfiable (equivalent to owl:Nothing).
// Distinct from test_disjoint_classes_shared_subclass, which requires a third witness class.
MATCH
  (ec:N {kind: 'EquivalentClasses'})-[:E {role: 'operand'}]->(a:N {kind: 'Class'}),
  (ec)-[:E {role: 'operand'}]->(b:N {kind: 'Class'}),
  (dc:N {kind: 'DisjointClasses'})-[:E {role: 'operand'}]->(a),
  (dc)-[:E {role: 'operand'}]->(b)
WHERE a.iri < b.iri
RETURN
  a.iri AS class_a,
  b.iri AS class_b
ORDER BY class_a, class_b

E106 test_same_different_individual

The same pair of individuals asserted both same and different.

// Individual pairs declared both SameIndividual and DifferentIndividuals.
// Direct contradiction regardless of the open- or closed-world assumption.
MATCH
  (si:N {kind: 'SameIndividual'})-[:E {role: 'operand'}]->(a:N {kind: 'NamedIndividual'}),
  (si)-[:E {role: 'operand'}]->(b:N {kind: 'NamedIndividual'}),
  (di:N {kind: 'DifferentIndividuals'})-[:E {role: 'operand'}]->(a),
  (di)-[:E {role: 'operand'}]->(b)
WHERE a.iri < b.iri
RETURN
  a.iri AS individual_a,
  b.iri AS individual_b
ORDER BY individual_a, individual_b

E107 test_negative_object_property_assertion_contradiction

An object property assertion and its explicit negation on the same source, property, and target.

// ObjectPropertyAssertion(P, a, b) and NegativeObjectPropertyAssertion(P, a, b) both
// exist for the same property, source, and target. A negative property assertion
// explicitly states the triple does NOT hold, so affirming it simultaneously is a direct
// contradiction regardless of the open- or closed-world assumption.
MATCH
  (pos:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p:N {kind: 'ObjectProperty'}),
  (pos)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (pos)-[:E {role: 'object'}]->(tgt:N {kind: 'NamedIndividual'}),
  (neg:N {kind: 'NegativeObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (neg)-[:E {role: 'subject'}]->(src),
  (neg)-[:E {role: 'object'}]->(tgt)
RETURN
  p.iri AS property_iri,
  src.iri AS source_iri,
  tgt.iri AS target_iri
ORDER BY property_iri, source_iri, target_iri

E108 test_negative_data_property_assertion_contradiction

A data property assertion and its explicit negation on the same source, property, and literal term. Literal matching is structural rather than value-normalized.

// DataPropertyAssertion(P, a, v) and NegativeDataPropertyAssertion(P, a, v) both exist
// for the same property, source individual, and literal term. This compares literal
// structure rather than projection node identity so duplicate literal nodes with the
// same value are still detected.
MATCH
  (pos:N {kind: 'DataPropertyAssertion'})-[:E {role: 'property'}]->(p:N {kind: 'DataProperty'}),
  (pos)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (pos)-[:E {role: 'object'}]->(pos_val:N {kind: 'Literal'}),
  (neg:N {kind: 'NegativeDataPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (neg)-[:E {role: 'subject'}]->(src),
  (neg)-[:E {role: 'object'}]->(neg_val:N {kind: 'Literal'})
WHERE pos_val.text = neg_val.text
  AND coalesce(pos_val.lang, '') = coalesce(neg_val.lang, '')
  AND coalesce(pos_val.datatype_iri, '') = coalesce(neg_val.datatype_iri, '')
RETURN
  p.iri AS property_iri,
  src.iri AS source_iri,
  pos_val.text
    + CASE WHEN pos_val.lang IS NULL THEN '' ELSE '@' + pos_val.lang END
    + CASE WHEN pos_val.datatype_iri IS NULL THEN '' ELSE '^^' + pos_val.datatype_iri END AS target_literal
ORDER BY property_iri, source_iri, target_literal

E109 test_property_contradictory_characteristics

Object properties declared with contradictory characteristics:

  • reflexive and irreflexive
  • symmetric and asymmetric
// Object properties declared with mutually contradictory characteristics.
// Reflexive + Irreflexive entails the property has no instances.
// Symmetric + Asymmetric entails the same.
// A reasoner catches these as inconsistencies, but only when the rest of the
// ontology is consistent enough to run.
MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'ReflexiveObjectProperty'})-[:E {role: 'property'}]->(p)
  }
  AND EXISTS {
    MATCH (:N {kind: 'IrreflexiveObjectProperty'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS property_iri, 'reflexive_and_irreflexive' AS contradiction

UNION ALL

MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'SymmetricObjectProperty'})-[:E {role: 'property'}]->(p)
  }
  AND EXISTS {
    MATCH (:N {kind: 'AsymmetricObjectProperty'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS property_iri, 'symmetric_and_asymmetric' AS contradiction

ORDER BY property_iri, contradiction

E110 test_irreflexive_property_self_assertion

An irreflexive object property explicitly asserted from an individual to itself.

// Object property assertions that violate an IrreflexiveObjectProperty declaration by
// relating an individual to itself.
MATCH (:N {kind: 'IrreflexiveObjectProperty'})-[:E {role: 'property'}]->(p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
MATCH
  (ax:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax)-[:E {role: 'subject'}]->(i:N {kind: 'NamedIndividual'}),
  (ax)-[:E {role: 'object'}]->(i)
RETURN
  p.iri AS property_iri,
  i.iri AS individual_iri
ORDER BY property_iri, individual_iri

E111 test_asymmetric_property_bidirectional_assertion

An asymmetric object property asserted in both directions between the same pair, or as a self-loop.

// Assertions that violate an AsymmetricObjectProperty declaration. Asymmetry means
// if P(a, b) holds then P(b, a) must not hold, and it also rules out self-loops
// because P(a, a) would imply its own converse.
MATCH (:N {kind: 'AsymmetricObjectProperty'})-[:E {role: 'property'}]->(p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
MATCH
  (ax:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax)-[:E {role: 'subject'}]->(a:N {kind: 'NamedIndividual'}),
  (ax)-[:E {role: 'object'}]->(b:N {kind: 'NamedIndividual'})
WHERE
  a = b
  OR (
    a.iri < b.iri
    AND EXISTS {
      MATCH
        (reverse:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
        (reverse)-[:E {role: 'subject'}]->(b),
        (reverse)-[:E {role: 'object'}]->(a)
    }
  )
RETURN DISTINCT
  p.iri AS property_iri,
  a.iri AS individual_a,
  b.iri AS individual_b
ORDER BY property_iri, individual_a, individual_b

E112 test_disjoint_object_properties_shared_assertion

The same subject/target pair asserted through two disjoint object properties.

// Object property assertions that violate a DisjointObjectProperties axiom by
// asserting the same subject/target pair through two disjoint object properties.
MATCH
  (dp:N {kind: 'DisjointObjectProperties'})-[:E {role: 'operand'}]->(p:N {kind: 'ObjectProperty'}),
  (dp)-[:E {role: 'operand'}]->(q:N {kind: 'ObjectProperty'}),
  (ax1:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax1)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (ax1)-[:E {role: 'object'}]->(tgt:N {kind: 'NamedIndividual'}),
  (ax2:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(q),
  (ax2)-[:E {role: 'subject'}]->(src),
  (ax2)-[:E {role: 'object'}]->(tgt)
WHERE p.iri < q.iri
RETURN
  p.iri AS property_a_iri,
  q.iri AS property_b_iri,
  src.iri AS source_iri,
  tgt.iri AS target_iri
ORDER BY property_a_iri, property_b_iri, source_iri, target_iri

E113 test_disjoint_data_properties_shared_assertion

The same subject/literal pair asserted through two disjoint data properties.

// Literal assertions that violate a DisjointDataProperties axiom by asserting the
// same subject/literal pair through two disjoint data properties. Literals compare
// by structure (text, language, datatype), not projection node identity.
MATCH
  (dp:N {kind: 'DisjointDataProperties'})-[:E {role: 'operand'}]->(p:N {kind: 'DataProperty'}),
  (dp)-[:E {role: 'operand'}]->(q:N {kind: 'DataProperty'}),
  (ax1:N {kind: 'DataPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax1)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (ax1)-[:E {role: 'object'}]->(v1:N {kind: 'Literal'}),
  (ax2:N {kind: 'DataPropertyAssertion'})-[:E {role: 'property'}]->(q),
  (ax2)-[:E {role: 'subject'}]->(src),
  (ax2)-[:E {role: 'object'}]->(v2:N {kind: 'Literal'})
WHERE p.iri < q.iri
  AND v1.text = v2.text
  AND coalesce(v1.lang, '') = coalesce(v2.lang, '')
  AND coalesce(v1.datatype_iri, '') = coalesce(v2.datatype_iri, '')
RETURN
  p.iri AS property_a_iri,
  q.iri AS property_b_iri,
  src.iri AS source_iri,
  v1.text
    + CASE WHEN v1.lang IS NULL THEN '' ELSE '@' + v1.lang END
    + CASE WHEN v1.datatype_iri IS NULL THEN '' ELSE '^^' + v1.datatype_iri END AS target_literal
ORDER BY property_a_iri, property_b_iri, source_iri, target_literal

E114 test_bottom_object_property_assertion

An assertion using owl:bottomObjectProperty, which can never have instances.

// Object property assertions against owl:bottomObjectProperty. The bottom object
// property is defined to have no extension, so any explicit assertion using it is
// impossible.
MATCH
  (ax:N {kind: 'ObjectPropertyAssertion'})
    -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2002/07/owl#bottomObjectProperty'}),
  (ax)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (ax)-[:E {role: 'object'}]->(tgt:N {kind: 'NamedIndividual'})
RETURN
  src.iri AS source_iri,
  tgt.iri AS target_iri
ORDER BY source_iri, target_iri

E115 test_bottom_data_property_assertion

An assertion using owl:bottomDataProperty, which can never have literal values.

// Data property assertions against owl:bottomDataProperty. The bottom data property
// is defined to have no extension, so any explicit assertion using it is impossible.
MATCH
  (ax:N {kind: 'DataPropertyAssertion'})
    -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2002/07/owl#bottomDataProperty'}),
  (ax)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (ax)-[:E {role: 'object'}]->(value:N {kind: 'Literal'})
RETURN
  src.iri AS source_iri,
  value.text
    + CASE WHEN value.lang IS NULL THEN '' ELSE '@' + value.lang END
    + CASE WHEN value.datatype_iri IS NULL THEN '' ELSE '^^' + value.datatype_iri END AS target_literal
ORDER BY source_iri, target_literal

Redundancy Warnings

These rules report low-noise structural cleanup opportunities.

W101 warn_subclass_reflexive

SubClassOf(A, A) tautologies. Legal OWL, but always redundant. Also fires — deliberately — when a DisjointUnion lists the union class among its own operands, which derives the same self-loop and is the same authoring accident wearing a different axiom.

// A class asserted to be a subclass of itself (A ⊑ A). Every class is trivially a
// subclass of itself, so this is redundant rather than inconsistent; it usually comes
// from editor round-trips or accidental copy/paste. Fires on the derived subclass_of
// self-loop, which comes from an asserted SubClassOf(C, C) or — deliberately — from a
// DisjointUnion that lists the union class among its own operands (the same authoring
// accident wearing a different axiom).
MATCH (c:N)-[:D {relation: 'subclass_of'}]->(c)
RETURN c.iri AS iri
ORDER BY iri

W102 warn_duplicate_subclass_axiom

Duplicate SubClassOf(A, B) pairs.

// The same SubClassOf(A, B) pair stated by more than one axiom. Report each repeated
// pair once rather than once per duplicate-axiom combination.
MATCH
  (ax:N {kind: 'SubClassOf'})-[:E {role: 'sub'}]->(sub:N {kind: 'Class'}),
  (ax)-[:E {role: 'super'}]->(sup:N {kind: 'Class'})
WITH sub, sup, count(*) AS axiom_count
WHERE axiom_count > 1
RETURN
  sub.iri AS sub_class,
  sup.iri AS super_class
ORDER BY sub_class, super_class

W103 warn_redundant_subclass_given_equivalence

SubClassOf(A, B) stated even though EquivalentClasses already entails it.

// SubClassOf(A, B) where an EquivalentClasses axiom already covers both A and B,
// making the SubClassOf entailed and redundant. Adds noise to the axiom set without
// adding information; editors and pipelines often create these unintentionally.
MATCH
  (ax:N {kind: 'SubClassOf'})-[:E {role: 'sub'}]->(a:N {kind: 'Class'}),
  (ax)-[:E {role: 'super'}]->(b:N {kind: 'Class'}),
  (ec:N {kind: 'EquivalentClasses'})-[:E {role: 'operand'}]->(a),
  (ec)-[:E {role: 'operand'}]->(b)
WHERE a.iri IS NOT NULL AND b.iri IS NOT NULL
RETURN DISTINCT
  a.iri AS sub_class,
  b.iri AS super_class
ORDER BY sub_class, super_class

W104 warn_disjoint_union_subclass_redundant

SubClassOf(Member, Parent) restated even though a DisjointUnion axiom already entails it.

// DisjointUnion(P, A, B, ...) already entails A ⊑ P, B ⊑ P, etc. as part of its
// semantics (it defines P as equivalent to the union of its members). A separate
// SubClassOf(A, P) is therefore redundant and adds noise without adding information.
// Parallel to warn_redundant_subclass_given_equivalence for EquivalentClasses.
MATCH
  (du:N {kind: 'DisjointUnion'})-[:E {role: 'class'}]->(parent:N {kind: 'Class'}),
  (du)-[:E {role: 'operand'}]->(member:N {kind: 'Class'}),
  (ax:N {kind: 'SubClassOf'})-[:E {role: 'sub'}]->(member),
  (ax)-[:E {role: 'super'}]->(parent)
WHERE parent.iri IS NOT NULL AND member.iri IS NOT NULL
RETURN DISTINCT
  parent.iri AS disjoint_union_parent,
  member.iri AS redundant_subclass_member
ORDER BY disjoint_union_parent, redundant_subclass_member

W105 warn_functional_property_multiple_values

Functional object properties used with two distinct named targets from the same source, excluding pairs already tied together with SameIndividual.

// Functional object properties where the same source individual has two distinct named
// target individuals. Under the Unique Name Assumption this is an inconsistency; without
// it a reasoner will silently merge the targets, which is almost always unintended.
MATCH (:N {kind: 'FunctionalObjectProperty'})-[:E {role: 'property'}]->(p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
MATCH
  (ax1:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax1)-[:E {role: 'subject'}]->(src:N {kind: 'NamedIndividual'}),
  (ax1)-[:E {role: 'object'}]->(t1:N {kind: 'NamedIndividual'}),
  (ax2:N {kind: 'ObjectPropertyAssertion'})-[:E {role: 'property'}]->(p),
  (ax2)-[:E {role: 'subject'}]->(src),
  (ax2)-[:E {role: 'object'}]->(t2:N {kind: 'NamedIndividual'})
WHERE ax1.uid < ax2.uid
  AND t1.iri <> t2.iri
  AND NOT EXISTS {
    MATCH
      (si:N {kind: 'SameIndividual'})-[:E {role: 'operand'}]->(t1),
      (si)-[:E {role: 'operand'}]->(t2)
  }
RETURN
  p.iri AS property_iri,
  src.iri AS source_individual_iri,
  t1.iri AS target_1_iri,
  t2.iri AS target_2_iri
ORDER BY property_iri, source_individual_iri, target_1_iri, target_2_iri

Supplemental Profiles

These rules still ship with Ontopoiesis, but they are not in the default baseline because they depend on modeling style, publication policy, or a stricter profile target.

editorial (P...)

Human-facing quality and publication guidance.

P101 warn_missing_label

Named entities (classes, properties, individuals, datatypes) with no rdfs:label. Without labels, entities are identified only by IRI in human-facing tools.

// Named entities with no rdfs:label. Without labels, entities are identified only by
// IRI in every human-facing tool, making the ontology unusable for most applications.
// Warning rather than error because a label-free ontology is structurally valid OWL;
// when imports are loaded, this fires for imported terms whose labels live elsewhere.
MATCH (e:N)
WHERE
  e.kind IN [
    'Class',
    'ObjectProperty',
    'DataProperty',
    'AnnotationProperty',
    'NamedIndividual',
    'Datatype'
  ]
  AND e.iri IS NOT NULL
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2000/01/rdf-schema#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2001/XMLSchema#'
  AND NOT EXISTS {
    MATCH
      (ax:N {kind: 'AnnotationAssertion'})
        -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2000/01/rdf-schema#label'}),
      (ax)-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'})
    WHERE subj.text = e.iri
  }
RETURN
  e.kind AS kind,
  e.iri AS iri
ORDER BY kind, iri

P102 warn_duplicate_label_language

Multiple rdfs:label annotations in the same language on the same entity. Usually a duplicate or accidental re-assertion rather than an intentional multilingual annotation.

// Multiple rdfs:label annotations in the same language on the same entity. OWL allows
// this, but applications can only display one label per language, and the choice is
// arbitrary. In practice this almost always indicates a copy-paste duplicate or an
// accidental re-assertion after an edit.
MATCH
  (ax:N {kind: 'AnnotationAssertion'})
    -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2000/01/rdf-schema#label'}),
  (ax)-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'}),
  (ax)-[:E {role: 'value'}]->(v:N {kind: 'Literal'})
WITH subj.text AS entity_iri, coalesce(v.lang, '') AS language_tag, count(ax) AS label_count
WHERE label_count > 1
RETURN entity_iri, language_tag, label_count
ORDER BY entity_iri, language_tag

P103 warn_labeled_without_definition

Named entities that have an rdfs:label but no definition from common annotation properties (skos:definition, IAO:0000115, dc:description, dcterms:description). Labeled-but-undefined terms are a common gap in publication-ready ontologies.

// Named entities that have an rdfs:label but no definition annotation from any of the
// common definition properties: skos:definition, IAO:0000115 (OBO), dc:description,
// dcterms:description. Catches entities that were named but not described.
MATCH (e:N)
WHERE
  e.kind IN [
    'Class',
    'ObjectProperty',
    'DataProperty',
    'AnnotationProperty',
    'NamedIndividual',
    'Datatype'
  ]
  AND e.iri IS NOT NULL
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2000/01/rdf-schema#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  AND EXISTS {
    MATCH
      (ax:N {kind: 'AnnotationAssertion'})
        -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2000/01/rdf-schema#label'}),
      (ax)-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'})
    WHERE subj.text = e.iri
  }
  AND NOT EXISTS {
    MATCH
      (ax:N {kind: 'AnnotationAssertion'})-[:E {role: 'property'}]->(prop:N),
      (ax)-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'})
    WHERE subj.text = e.iri
      AND prop.iri IN [
        'http://www.w3.org/2004/02/skos/core#definition',
        'http://purl.obolibrary.org/obo/IAO_0000115',
        'http://purl.org/dc/elements/1.1/description',
        'http://purl.org/dc/terms/description'
      ]
  }
RETURN e.kind AS kind, e.iri AS iri
ORDER BY kind, iri

P104 warn_annotation_assertion_unknown_subject

AnnotationAssertion axioms whose subject IRI does not appear elsewhere in the ontology as a declared entity or as the ontology IRI itself. These are genuinely dangling annotations — not merely imported terms, which would appear at least as references in other axioms.

// AnnotationAssertion axioms whose subject IRI does not appear anywhere else in the
// ontology graph as either an entity IRI or the ontology IRI. This is intentionally
// weaker than "missing Declaration": OWL 2 allows undeclared entities, so the warning
// should focus on genuinely dangling annotation subjects rather than profile style.
// Only full-IRI subjects are checked: anonymous-individual subjects are always local,
// and abbreviated-IRI subjects would need prefix expansion to compare.
MATCH (ax:N {kind: 'AnnotationAssertion'})-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'})
WHERE subj.text IS NOT NULL
  AND NOT subj.text STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT subj.text STARTS WITH 'http://www.w3.org/2000/01/rdf-schema#'
  AND NOT subj.text STARTS WITH 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  AND NOT subj.text STARTS WITH 'http://www.w3.org/2001/XMLSchema#'
  AND NOT EXISTS {
    MATCH (n:N)
    WHERE n.iri = subj.text OR n.ontology_iri = subj.text
  }
RETURN DISTINCT subj.text AS subject_iri
ORDER BY subject_iri

P105 warn_deprecated_entity_referenced

Entities marked owl:deprecated that still appear in structural axioms or class expressions (excluding their own Declaration). Deprecated entities should be phased out of active use; continued structural reference is usually an oversight.

// Entities marked owl:deprecated that still appear in structural axioms or class
// expressions. Excludes the entity's own Declaration (which is expected to remain).
// This is useful migration guidance, but not a universal logical contradiction, so it
// lives in the editorial profile rather than the default baseline.
MATCH (e:N)
WHERE
  e.kind IN [
    'Class',
    'ObjectProperty',
    'DataProperty',
    'AnnotationProperty',
    'NamedIndividual'
  ]
  AND e.iri IS NOT NULL
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND EXISTS {
    MATCH
      (dep:N {kind: 'AnnotationAssertion'})
        -[:E {role: 'property'}]->(:N {iri: 'http://www.w3.org/2002/07/owl#deprecated'}),
      (dep)-[:E {role: 'subject'}]->(subj:N {kind: 'IRI'})
    WHERE subj.text = e.iri
  }
  AND EXISTS {
    MATCH (ref:N)-[:E]->(e)
    WHERE ref.kind <> 'Declaration'
  }
RETURN e.kind AS kind, e.iri AS iri
ORDER BY kind, iri

P106 warn_entity_iri_equals_ontology_iri

Named entities whose IRI is identical to the ontology IRI itself. OWL 2 permits this but it conflates the ontology document with a term in its own vocabulary, which causes consistent confusion in tools and downstream consumers.

// Entities whose IRI is identical to the ontology IRI itself. This is often confusing
// in published vocabularies, but some projects do it intentionally, so keep it out of
// the default universal baseline.
MATCH (ont:N {kind: 'Ontology'}), (e:N)
WHERE ont.ontology_iri IS NOT NULL
  AND e.iri = ont.ontology_iri
  AND e.kind IN [
    'Class',
    'ObjectProperty',
    'DataProperty',
    'AnnotationProperty',
    'NamedIndividual',
    'Datatype'
  ]
RETURN e.kind AS kind, e.iri AS iri
ORDER BY kind, iri

P107 warn_version_iri_missing

Ontologies that have an IRI but no version IRI. Without a version IRI, consumers cannot distinguish between revisions when following the ontology IRI across releases.

// Ontology has an IRI but no version IRI. Without a version IRI, consumers cannot
// distinguish between revisions of the same ontology.
MATCH (ont:N {kind: 'Ontology'})
WHERE ont.ontology_iri IS NOT NULL
  AND ont.version_iri IS NULL
RETURN ont.ontology_iri AS ontology_iri

P108 warn_ontology_no_metadata_annotations

Ontologies with no common metadata annotations (title, creator, description, license). An ontology without provenance metadata is difficult to cite, attribute, or evaluate for reuse.

// Ontology has no common metadata annotations such as title, creator, description, or
// license. Metadata annotations are essential for provenance and discovery.
MATCH (ont:N {kind: 'Ontology'})
WHERE ont.ontology_iri IS NOT NULL
  AND NOT EXISTS {
    MATCH
      (ont)-[:E {role: 'annotation'}]->(ann:N {kind: 'Annotation'}),
      (ann)-[:E {role: 'property'}]->(prop:N {kind: 'AnnotationProperty'})
    WHERE prop.iri IN [
      'http://purl.org/dc/elements/1.1/title',
      'http://purl.org/dc/elements/1.1/creator',
      'http://purl.org/dc/elements/1.1/description',
      'http://purl.org/dc/elements/1.1/license',
      'http://purl.org/dc/terms/title',
      'http://purl.org/dc/terms/creator',
      'http://purl.org/dc/terms/description',
      'http://purl.org/dc/terms/license'
    ]
  }
RETURN ont.ontology_iri AS ontology_iri

modeling_risk (M...)

Valid OWL that often deserves explicit review.

M101 warn_subclass_cycle

Subclass cycles: a chain of named classes A ⊑ B ⊑ … ⊑ A. Legal OWL 2 (it implies the members are equivalent), but almost always an accidental hierarchy-maintenance error rather than an intended equivalence. Reports each class that participates in a cycle. Cycle length is bounded to 5 using fixed-length patterns over the derived subclass_of edges: real accidental cycles are short, and unbounded recursive traversal of the whole hierarchy is prohibitively slow on large ontologies.

// Subclass cycles: a chain of named classes A ⊑ B ⊑ … ⊑ A. Legal OWL — it collapses the
// members into one equivalence set — but almost always an accidental hierarchy-maintenance
// error rather than an intended equivalence. Reports each class that participates in a cycle.
//
// Cycle length is bounded to 5 with fixed-length patterns: real accidental cycles are short,
// and unbounded recursive traversal over the whole hierarchy is prohibitively slow on large
// ontologies. Each branch reports every member because the anchor rotates over all matches.
MATCH (a:N)-[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(a)
RETURN DISTINCT a.iri AS iri
UNION
MATCH (a:N)-[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(a)
RETURN DISTINCT a.iri AS iri
UNION
MATCH (a:N)-[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(a)
RETURN DISTINCT a.iri AS iri
UNION
MATCH (a:N)-[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(:N)
      -[:D {relation: 'subclass_of'}]->(a)
RETURN DISTINCT a.iri AS iri
ORDER BY iri

M103 warn_object_property_no_domain_or_range

Object properties with neither ObjectPropertyDomain nor ObjectPropertyRange. Properties that declare only a domain or only a range are not flagged — this rule targets properties with no semantic scope at all.

// Object properties with neither an ObjectPropertyDomain nor an ObjectPropertyRange
// axiom. Without either, the property is unconstrained: reasoners cannot infer class
// membership from its usage, and documentation tools have no structural context to
// display. Properties with only domain or only range declared are not flagged.
MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND NOT p.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT EXISTS {
    MATCH (:N {kind: 'ObjectPropertyDomain'})-[:E {role: 'property'}]->(p)
  }
  AND NOT EXISTS {
    MATCH (:N {kind: 'ObjectPropertyRange'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS iri
ORDER BY iri

M104 warn_datatype_property_no_domain

Data properties with no DataPropertyDomain axiom. Without a domain, the property can be asserted on any individual regardless of type, often an incomplete ABox modeling decision.

// Data properties with no DataPropertyDomain axiom. Without a declared domain,
// the property can be asserted on any individual regardless of type, and reasoners
// cannot infer class membership from its usage. Parallel to the domain side of
// warn_object_property_no_domain_or_range.
MATCH (p:N {kind: 'DataProperty'})
WHERE p.iri IS NOT NULL
  AND NOT p.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT EXISTS {
    MATCH (:N {kind: 'DataPropertyDomain'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS iri
ORDER BY iri

M105 warn_datatype_property_no_range

Data properties with no DataPropertyRange axiom. Without a range, the property accepts any literal value with no type constraint, which weakens data quality checks downstream.

// Data properties with no DataPropertyRange axiom. Without a declared range the
// property accepts any literal value, which usually indicates an incomplete schema.
// Parallel to warn_object_property_no_domain_or_range for object properties.
MATCH (p:N {kind: 'DataProperty'})
WHERE p.iri IS NOT NULL
  AND NOT p.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT EXISTS {
    MATCH (:N {kind: 'DataPropertyRange'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS iri
ORDER BY iri

M106 warn_individual_no_type

Named individuals with no ClassAssertion. A declared but untyped individual is a common incomplete ABox pattern — the individual exists in the ontology but contributes no classifiable information.

// Named individuals with no ClassAssertion — present in the signature but untyped.
// Under the open-world assumption this is not an error, but it commonly indicates
// an incomplete ABox or an individual that was declared and then forgotten.
MATCH (i:N {kind: 'NamedIndividual'})
WHERE i.iri IS NOT NULL
  AND NOT EXISTS {
    MATCH (:N {kind: 'ClassAssertion'})-[:E {role: 'individual'}]->(i)
  }
RETURN i.iri AS iri
ORDER BY iri

M107 warn_property_dangerous_combination

Object properties combining Transitive with Functional or InverseFunctional. Transitivity collapses entire chains to a single filler, making a functional property over a transitive chain almost always unsatisfiable or unexpectedly restrictive.

// Object properties with characteristic combinations that produce strong and often
// unintended consequences under reasoning:
//   Transitive + Functional: collapses the entire forward chain to a single target value.
//   Transitive + InverseFunctional: collapses the entire backward chain to a single source.
//   Functional + InverseFunctional: implies a bijection between domain and range instances.
MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'TransitiveObjectProperty'})-[:E {role: 'property'}]->(p)
  }
  AND EXISTS {
    MATCH (:N {kind: 'FunctionalObjectProperty'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS property_iri, 'transitive_and_functional' AS combination

UNION ALL

MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'TransitiveObjectProperty'})-[:E {role: 'property'}]->(p)
  }
  AND EXISTS {
    MATCH (:N {kind: 'InverseFunctionalObjectProperty'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS property_iri, 'transitive_and_inverse_functional' AS combination

UNION ALL

MATCH (p:N {kind: 'ObjectProperty'})
WHERE p.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'FunctionalObjectProperty'})-[:E {role: 'property'}]->(p)
  }
  AND EXISTS {
    MATCH (:N {kind: 'InverseFunctionalObjectProperty'})-[:E {role: 'property'}]->(p)
  }
RETURN p.iri AS property_iri, 'functional_and_inverse_functional' AS combination

ORDER BY property_iri, combination

M108 warn_annotation_punning_object_property

An IRI used simultaneously as both an ObjectProperty and an AnnotationProperty. OWL 2 DL explicitly permits this (annotation punning), but axioms written against the object property role are not visible to tools that traverse the same IRI as an annotation property, and vice versa.

// IRI used simultaneously as an ObjectProperty and an AnnotationProperty.
// OWL 2 DL explicitly permits this (annotation punning), but the overlap creates
// a semantic gap: axioms written against the object property role are invisible
// to tools that traverse the same IRI as an annotation property, and vice versa.
MATCH
    (o:N {kind: 'ObjectProperty'}),
    (a:N {kind: 'AnnotationProperty'})
WHERE o.iri = a.iri
RETURN o.iri AS iri
ORDER BY iri

M109 warn_annotation_punning_data_property

An IRI used simultaneously as both a DataProperty and an AnnotationProperty. OWL 2 DL explicitly permits this (annotation punning), but range constraints and typing rules declared on the data property are not enforced when the same IRI is accessed through annotation traversal.

// IRI used simultaneously as a DataProperty and an AnnotationProperty.
// OWL 2 DL explicitly permits this (annotation punning), but range constraints
// and typing rules declared against the data property role are not enforced when
// the same IRI is accessed through annotation traversal.
MATCH
    (d:N {kind: 'DataProperty'}),
    (a:N {kind: 'AnnotationProperty'})
WHERE d.iri = a.iri
RETURN d.iri AS iri
ORDER BY iri

description_logic (D...)

Checks that are useful when you specifically want a stricter OWL 2 DL-oriented surface.

D101 test_undeclared_entities

Named entities (other than built-in OWL 2 IRIs) used in axioms without a corresponding Declaration axiom. OWL 2 DL requires all named entities to be explicitly declared; undeclared use is a syntactic violation that some parsers silently accept. In Ontopoiesis's current source-document projection model, this rule reports declarations missing from the built projection. If the entity belongs to an imported ontology that was not merged before build, treat the result as closure-dependent rather than as a confirmed ontology-wide defect.

// Named entities other than Datatype must have a Declaration axiom.
// Entities from the OWL 2 built-in namespaces (owl:, rdfs:, rdf:) are
// pre-declared by the OWL 2 spec and never require explicit Declarations.
MATCH (e:N)
WHERE
  e.kind IN [
    'Class',
    'ObjectProperty',
    'DataProperty',
    'AnnotationProperty',
    'NamedIndividual'
  ]
  AND e.iri IS NOT NULL
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2002/07/owl#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/2000/01/rdf-schema#'
  AND NOT e.iri STARTS WITH 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  AND NOT EXISTS {
    MATCH (:N {kind: 'Declaration'})-[:E {role: 'entity'}]->(e)
  }
RETURN e.kind AS kind, e.iri AS iri

UNION ALL

// User-defined datatypes (those with a DatatypeDefinition axiom) must also be
// declared. Built-in XSD/RDF/OWL datatypes are pre-declared by the OWL 2 spec
// and are intentionally excluded here by only checking for DatatypeDefinition.
MATCH (e:N {kind: 'Datatype'})
WHERE
  e.iri IS NOT NULL
  AND EXISTS {
    MATCH (:N {kind: 'DatatypeDefinition'})-[:E {role: 'datatype'}]->(e)
  }
  AND NOT EXISTS {
    MATCH (:N {kind: 'Declaration'})-[:E {role: 'entity'}]->(e)
  }
RETURN
  e.kind AS kind,
  e.iri AS iri

ORDER BY kind, iri

D102 test_punning_class_individual

An IRI used simultaneously as both a Class and a NamedIndividual. OWL 2 DL disallows this form of punning; it almost always indicates a conflation of the class taxonomy with its instance data.

// IRI used simultaneously as a Class and a NamedIndividual. OWL 2 permits punning
// in limited contexts, but Class/Individual punning almost always results from a
// copy-paste error or from conflating the taxonomy with instance data, and causes
// reasoning anomalies when the same IRI is interpreted as both a concept and an instance.
MATCH
    (c:N {kind: 'Class'}),
    (i:N {kind: 'NamedIndividual'})
WHERE c.iri = i.iri
RETURN c.iri AS iri
ORDER BY iri

D103 test_punning_class_datatype

An IRI used simultaneously as both a Class and a Datatype. These inhabit disjoint domains in OWL 2; the combination is illegal in OWL 2 DL and will be rejected by conformant reasoners.

// IRI used simultaneously as a Class and a Datatype. Classes inhabit the object
// domain; datatypes inhabit the data domain. These roles are disjoint in OWL 2
// semantics, so an IRI spanning both is almost always a namespace collision or
// copy-paste error and cannot be used coherently in either context.
MATCH
    (c:N {kind: 'Class'}),
    (d:N {kind: 'Datatype'})
WHERE c.iri = d.iri
RETURN c.iri AS iri
ORDER BY iri

D104 test_punning_object_data_property

An IRI used simultaneously as both an ObjectProperty and a DataProperty. These roles are disjoint in OWL 2; a property cannot relate individuals to individuals and individuals to literals at the same time.

// IRI used simultaneously as an ObjectProperty and a DataProperty. Object properties
// relate individuals to individuals; data properties relate individuals to literals.
// These roles are disjoint in OWL 2 — a single IRI cannot serve both, and any axiom
// using it will be interpreted under one role only, silently ignoring the other.
MATCH
    (o:N {kind: 'ObjectProperty'}),
    (d:N {kind: 'DataProperty'})
WHERE o.iri = d.iri
RETURN o.iri AS iri
ORDER BY iri

D105 test_disjoint_classes_shared_subclass_transitive

A named class that is a subclass — at any depth — of two classes declared disjoint. Such a class is unsatisfiable (equivalent to owl:Nothing), inheriting from two provably disjoint parents. This generalizes the direct check E103 through the derived subclass_of closure, catching deep unsatisfiabilities a reasoner would flag but a one-hop structural check misses. It also covers the degenerate case of a class that is disjoint with one of its own ancestors, which the shared-subclass pattern alone cannot match. It walks descendants from the (few) disjoint pairs to stay tractable, with depth capped at 20, and lives in the opt-in description_logic profile because the closure walk is heavier than the default baseline. It subsumes E103, so direct cases also appear here when both are run.

// A class that is a subclass — at ANY depth — of two classes declared disjoint is
// unsatisfiable (equivalent to owl:Nothing): it inherits from two provably disjoint
// parents. This generalizes the direct check E103 through the derived subclass_of closure,
// catching deep unsatisfiabilities that a reasoner would flag but a one-hop structural
// check misses. It is anchored on the (few) disjoint pairs and then walks descendants, so
// it stays tractable; depth is capped at 20. Kept in the opt-in description_logic profile
// because the closure walk is heavier than the default baseline checks. It subsumes E103,
// so the direct cases also appear here when both are run.
MATCH (a:N)-[:D {relation: 'disjoint_class'}]->(b:N)
WHERE a.iri < b.iri
MATCH (c:N)-[:D*1..20 {relation:'subclass_of'}]->(a),
      (c)-[:D*1..20 {relation:'subclass_of'}]->(b)
RETURN DISTINCT c.iri AS unsatisfiable_class, a.iri AS disjoint_a, b.iri AS disjoint_b
UNION
// Degenerate case: a disjoint class that is itself a subclass (at any depth) of the class
// it is disjoint with. The branch above needs a path of length >= 1 to BOTH parents, so
// c = a never matches there. Both directions of disjoint_class are materialized, so this
// single directed pattern catches a below b and b below a alike.
MATCH (a:N)-[:D {relation: 'disjoint_class'}]->(b:N)
MATCH (a)-[:D*1..20 {relation:'subclass_of'}]->(b)
RETURN DISTINCT a.iri AS unsatisfiable_class,
       CASE WHEN a.iri < b.iri THEN a.iri ELSE b.iri END AS disjoint_a,
       CASE WHEN a.iri < b.iri THEN b.iri ELSE a.iri END AS disjoint_b
ORDER BY unsatisfiable_class, disjoint_a, disjoint_b