Ontology Quality Suite · check reference

The Check Registry

Every quality and conformance check the suite runs, what each one actually asserts, which of them are written in SPARQL and which in SHACL — and how to add your own to the panel without forking anything.

56checks in the registry
42SPARQL formulations
21with a SHACL twin
14native Python checks

How the panel runs

One registry, three ways of producing a finding. Everything downstream — the HTML report, the CSV, the Cucumber features, the severity gate that sets the exit code — consumes a single row type, so where a finding came from never leaks into how it is reported.

registry.json is the single source of truth. It holds no logic: each entry is an id, a category, a severity, the prose shown to whoever reads the finding, and the Cucumber feature/scenario names. A check's implementation lives elsewhere, and there are three kinds:

KindLives inCountUse when
Portable SPARQLsparql/<category>/<id>.rq42 The condition is a graph pattern over one merged graph. This is the default and covers most checks.
SHACL shapeshapes/<category>.ttl21 Always paired with a SPARQL twin, never alone — the pair exists to cross-validate.
Native Pythonreasoning/, dataquality/14 There is no single graph to pattern-match: profile membership, an external reasoner's verdict, or a comparison against a separate ontology.

Discovery is by directory walk, not by manifest. sparql_runner globs sparql/**/*.rq and shacl_runner loads every shapes/*.ttl, so adding a file is enough to add a check and deleting one is enough to remove it. The registry entry supplies the prose; a query with no matching entry, or an entry with no implementation, is the kind of drift the coverage tests exist to catch.

The result contract

Both engines are made to speak SHACL's own vocabulary, which is what lets their output merge. A portable SPARQL check is a CONSTRUCT that builds a sh:ValidationResult by hand:

CONSTRUCT {
  _:r a sh:ValidationResult ;
    sh:resultSeverity sh:Warning ;
    sh:focusNode ?p ;
    sh:sourceConstraintComponent oq:STR-003 ;
    sh:resultMessage ?msg .
}
WHERE {
  { ?p a owl:ObjectProperty } UNION { ?p a owl:DatatypeProperty }
  FILTER NOT EXISTS { ?p rdfs:domain ?d }
  FILTER NOT EXISTS { ?p rdfs:range ?r }
  BIND(CONCAT("Property ", STR(?p), " declares neither domain nor range.") AS ?msg)
}

Four properties are mandatory: sh:resultSeverity, sh:focusNode, sh:resultMessage, and sh:sourceConstraintComponent oq:<id> — that last one is how a bare CONSTRUCT result gets tied back to its registry entry. Add sh:resultPath and sh:value when there is a natural predicate or offending value. Binding several of either on one result is legitimate and sometimes the honest thing to do: LOG-004 names both inverses it is complaining about. They are sorted and joined on the way in, so a finding renders identically however the engine happened to order them.

Everything then normalises to one ResultRow:

ResultRow(check_id, category, title, severity,
          focus_node, path, value,
          message, remediation, sources)

Rows are deduplicated on (check_id, focus_node, path, value). When both engines find the same thing, one row survives carrying sources: ["shacl", "sparql"] — which is precisely the signal described next.

Why two formulations of the same check

Eighteen checks are written twice: once as a SPARQL CONSTRUCT, once as a SHACL shape. The duplication is deliberate. Running both and merging on that dedup key means a check that fires from one engine but not the other shows up as a row with only one entry in sources — a visible sign the two formulations have drifted apart, rather than a silent disagreement about what the check means.

--engine selects what actually runs:

ValueRunsNotes
bothpyshacl + portable SPARQLFull cross-validation. The default when the native engine is absent.
sparqlportable SPARQL onlySame real findings, no drift signal, roughly 8× faster.
shaclpyshacl onlyFor symmetry; strictly slower with no broader coverage.
nativenative Rust SHACL engineOptional package. Verified to find exactly what pyshacl finds.
native+sparqlnative + portable SPARQLThe fast analogue of both. Default when installed.
Why sparql-only is safe

Every check in the registry-driven suite has a SPARQL formulation; only a subset also has a SHACL one, and there is no check implemented in SHACL alone. So --engine sparql finds the same set of real findings — it forfeits the drift signal, not coverage. A test asserts the SHACL-only set stays empty, because a future SHACL-only check would quietly break that guarantee.

The speed gap is not subtle: measured over a real 3,300-triple ontology, the same ~50-check pass took about 193 seconds under pyshacl against about 27 seconds for the portable layer, because pyshacl spends most of its time on Python-level shape traversal layered on top of the same SPARQL execution. Cross-validate in CI; use sparql or native+sparql while iterating.

The catalogue

All 56 checks, grouped by category. Severity is the registry default and can be overridden per project; the badges say which formulations exist for each check and where to find them.

Structural integrity

structural · 9 checks · 7 violation · 2 warning

Does every term the graph uses actually resolve to something declared? These are the referential-integrity checks -- the cheapest to run and the ones that catch typos, stale namespaces and half-finished refactors before anything subtler is worth looking at.

STR-001

Undefined class used as rdf:type

Violation

referential integrity of class usage

An individual is typed with a class IRI that is never declared as owl:Class or rdfs:Class anywhere in the combined ontology+data graph.

FixDeclare the class with 'CLASS a owl:Class', import the vocabulary that defines it, or fix the typo in the type IRI.

SPARQLSHACL

sparql/structural/STR-001.rq · shapes/structural.ttl

STR-002

Undefined property used

Violation

referential integrity of property usage

A predicate is used in a triple but is never declared as rdf:Property, owl:ObjectProperty, owl:DatatypeProperty or owl:AnnotationProperty.

FixDeclare the property's type explicitly, or import the vocabulary that defines it.

SPARQLSHACL

sparql/structural/STR-002.rq · shapes/structural.ttl

STR-003

Property missing both domain and range

Warning

schema completeness

An owl:ObjectProperty or owl:DatatypeProperty declares neither rdfs:domain nor rdfs:range.

FixAdd rdfs:domain and rdfs:range (or an equivalent OWL restriction) to make the property's intended usage explicit and enable stronger reasoning/validation.

SPARQLSHACL

sparql/structural/STR-003.rq · shapes/data.ttl

STR-004

Class has no formal definition

Warning

schema completeness

An owl:Class has none of owl:equivalentClass, owl:intersectionOf, rdfs:subClassOf, owl:unionOf, owl:oneOf or owl:disjointWith, leaving it without any formal definition.

FixGive the class a formal definition, e.g. an rdfs:subClassOf axiom, an owl:equivalentClass restriction, or an explicit union/intersection/enumeration.

SPARQL

sparql/structural/STR-004.rq

STR-005

Property domain value has no declared type

Violation

referential integrity of schema metadata

An rdfs:domain value is never given an rdf:type anywhere in the graph.

FixDeclare the domain value as a class (or fix the typo/reference), so the domain axiom actually points at a real class.

SPARQL

sparql/structural/STR-005.rq

STR-006

Untyped object of a triple

Violation

referential integrity of instance usage

The IRI object of a triple (excluding owl:imports/owl:versionIRI/rdf:type) is never given an rdf:type anywhere in the graph.

FixDeclare an rdf:type for the referenced resource, or fix the IRI if it was a typo.

SPARQL

sparql/structural/STR-006.rq

STR-007

Predicate has no declared rdf:type

Violation

referential integrity of property usage

A predicate used in a triple is never given any rdf:type at all (a broader check than STR-002, which only looks for the four standard property types).

FixDeclare an rdf:type for the predicate (e.g. rdf:Property or a more specific OWL property type), or import the vocabulary that defines it.

SPARQL

sparql/structural/STR-007.rq

STR-008

Property range value has no declared type

Violation

referential integrity of schema metadata

An owl:ObjectProperty's rdfs:range value is never given an rdf:type anywhere in the graph.

FixDeclare the range value as a class (or fix the typo/reference), so the range axiom actually points at a real class.

SPARQL

sparql/structural/STR-008.rq

STR-009

Untyped subject of a triple

Violation

referential integrity of instance usage

The subject of a triple is never given an rdf:type anywhere in the graph.

FixDeclare an rdf:type for the subject, or fix the IRI if it was a typo.

SPARQL

sparql/structural/STR-009.rq

Logical cogency

logical · 7 checks · 6 violation · 1 warning

Do the axioms contradict each other, or does the data contradict the axioms? These read the ontology as a set of claims and look for pairs that cannot both hold.

LOG-001

Class disjoint with its own ancestor

Violation

logical consistency / cogency

A class is asserted owl:disjointWith one of its own transitive rdfs:subClassOf ancestors, which makes the class logically unsatisfiable.

FixRemove either the subclass axiom or the disjointness axiom; the two together are contradictory.

SPARQLSHACLre-run post-closure

sparql/logical/closure-safe/LOG-001.rq · shapes/logical.ttl

LOG-002

owl:FunctionalProperty violated by data

Violation

logical consistency of data w.r.t. axioms

A property declared owl:FunctionalProperty has a subject with two or more distinct values, contradicting the functional-property axiom.

FixEither correct the data so each subject has a single value, or remove the FunctionalProperty axiom if multiple values are legitimate.

SPARQLSHACLre-run post-closure

sparql/logical/closure-safe/LOG-002.rq · shapes/logical.ttl

LOG-003

Redundant equivalentClass + subClassOf pair

Warning

axiom redundancy / hidden cycles

A class is declared both owl:equivalentClass and rdfs:subClassOf the same other class, which is logically redundant and may mask an unintended modelling cycle.

FixKeep only owl:equivalentClass (which already implies subclassing both ways) or reconsider whether subclassing was intended instead of equivalence.

SPARQLSHACL

sparql/logical/LOG-003.rq · shapes/logical.ttl

LOG-004

Property has more than one inverse

Violation

logical consistency of inverse axioms

A property is declared owl:inverseOf two distinct other properties (directly or indirectly), which is contradictory since an inverse pairing must be unique.

FixRemove the extra owl:inverseOf assertion so the property has at most one declared inverse.

SPARQLre-run post-closure

sparql/logical/closure-safe/LOG-004.rq

LOG-005

Property declared inverse of itself

Violation

logical consistency of inverse axioms

A property is asserted owl:inverseOf itself, a degenerate/contradictory inverse axiom.

FixRemove the self-referential owl:inverseOf assertion.

SPARQLre-run post-closure

sparql/logical/closure-safe/LOG-005.rq

LOG-006

Symmetric property has unequal domain and range

Violation

logical consistency of symmetric properties

A property declared owl:SymmetricProperty has an rdfs:domain different from its rdfs:range, which is inconsistent for a symmetric relation.

FixMake the domain and range equal (e.g. both the same class, or their union), or remove the SymmetricProperty axiom if that is not intended.

SPARQL

sparql/logical/LOG-006.rq

LOG-007

Transitive property has unequal domain and range

Violation

logical consistency of transitive properties

A property declared owl:TransitiveProperty has an rdfs:domain different from its rdfs:range, which is inconsistent for a transitive relation.

FixMake the domain and range equal (e.g. both the same class, or their union), or remove the TransitiveProperty axiom if that is not intended.

SPARQL

sparql/logical/LOG-007.rq

Documentation and metadata quality

quality · 10 checks · 8 warning · 2 info

Is the ontology usable by someone who did not write it? Labels, definitions, versioning and a coherent identity for the ontology as a published resource.

QUA-001

Class or property missing rdfs:label

Warning

documentation effectiveness

A class, object property or datatype property has no rdfs:label, reducing human readability of the ontology.

FixAdd at least one rdfs:label, ideally with a language tag, e.g. rdfs:label "Person"@en.

SPARQLSHACL

sparql/quality/QUA-001.rq · shapes/quality.ttl

QUA-002

Ontology missing versioning/title metadata

Info

provenance / effectiveness metadata

The owl:Ontology node has no owl:versionInfo, dcterms:title or rdfs:label, harming provenance tracking and long-term effectiveness.

FixAdd owl:versionInfo, dcterms:title/dcterms:description, and dcterms:created/modified annotations to the ontology header.

SPARQLSHACL

sparql/quality/QUA-002.rq · shapes/quality.ttl

QUA-003

Deprecated term still in active use

Warning

lifecycle hygiene

A term marked owl:deprecated true is still used as a type or predicate elsewhere in the graph.

FixMigrate usages to the replacement term (if any, e.g. via dcterms:isReplacedBy) and stop using the deprecated term in new data.

SPARQLSHACL

sparql/quality/QUA-003.rq · shapes/quality.ttl

QUA-004

Resource missing skos:prefLabel

Warning

documentation effectiveness

A non-W3C-namespace resource used somewhere in the graph has no skos:prefLabel, reducing human readability and documentation quality.

FixAdd a skos:prefLabel to the resource, ideally with a language tag.

SPARQL

sparql/quality/QUA-004.rq

QUA-005

Ontology has no identifying IRI at all

Warning

ontology identity

No resource in the graph is declared 'a owl:Ontology'. Distinct from QUA-002, which only fires once an owl:Ontology resource exists but lacks title/version/label metadata on it -- this fires when there's no such resource whatsoever.

FixAdd '<ontologyIRI> a owl:Ontology .' identifying the ontology as a whole, distinct from any class/property IRI it defines.

SPARQL

sparql/quality/QUA-005.rq

QUA-006

Ontology IRI reused as the namespace IRI for its own concepts

Warning

ontology identity

The ontology's own identifying IRI (the subject of 'a owl:Ontology') is, once a class/property IRI's local name is stripped off, identical to that concept's namespace -- i.e. the ontology's identity and the namespace minting its terms are the same IRI. Best practice keeps these distinct (e.g. an ontology IRI with no trailing '#'/'/', and a namespace IRI that has one), so tooling and humans can tell 'the ontology as a resource' apart from 'a term it defines'.

FixGive the ontology its own identifying IRI distinct from the namespace IRI used to mint class/property IRIs (commonly: same base IRI, but the ontology IRI omits the trailing '#'/'/' that the namespace IRI has).

SPARQL

sparql/quality/QUA-006.rq

QUA-007

Ontology missing owl:versionIRI

Info

ontology versioning

The ontology has no owl:versionIRI, making it harder for consumers/tools to pin, cache-bust, or detect exactly which version of the ontology they are using.

FixAdd an owl:versionIRI, distinct per released version (e.g. embedding a version segment in the IRI path).

SPARQL

sparql/quality/QUA-007.rq

QUA-008

Ontology or version IRI does not use the https:// scheme

Warning

ontology identity

The ontology's identifying IRI, or its owl:versionIRI, uses a scheme other than https:// (typically plain http://). Using https guarantees the integrity/authenticity of the ontology document when it is actually dereferenced.

FixMint the ontology IRI and versionIRI under an https:// base.

SPARQL

sparql/quality/QUA-008.rq

QUA-009

Class or property without one skos:prefLabel per language

Warning

term documentation completeness

A declared class or property has no skos:prefLabel, or has more than one for a single language. Not 'exactly one' outright: SKOS defines prefLabel as unique per language tag, so a bilingual ontology carrying "Road"@en and "Ffordd"@cy is correct and flagging it would be wrong about SKOS rather than strict about it. An untagged literal counts as its own language -- RDF 1.1 gives it the datatype xsd:string and no tag, so 'the unlocalised label' is a real slot with room for one value. That case needs stating because SHACL's own sh:uniqueLang ignores untagged values, and untagged is how gist-based ontologies label everything. Stricter and narrower than QUA-004, which accepts an rdfs:label instead and only asks that some label exist. Anonymous class expressions are exempt: they are owl:Class instances that can never carry a label.

FixAdd a skos:prefLabel if the term has none. If it has several sharing one language (including several with no language tag at all), keep the canonical one for that language and move the rest to skos:altLabel.

SPARQLSHACL

sparql/quality/QUA-009.rq · shapes/quality.ttl

QUA-010

Class or property without a skos:definition

Warning

term documentation completeness

A declared class or property has no skos:definition. Distinct from STR-004, which asks whether a class is formally defined by an axiom (rdfs:subClassOf, owl:equivalentClass, a union/intersection/enumeration): that is a question about logic, this one is about prose, and a term can be fully axiomatised and still leave a reader unable to tell what it means. skos:definition specifically, not rdfs:comment -- a comment is a note to whoever maintains the ontology, a definition is the term's meaning.

FixAdd a skos:definition stating what the term means, in prose a reader outside the authoring team can act on.

SPARQLSHACL

sparql/quality/QUA-010.rq · shapes/quality.ttl

Structural and runtime efficiency

efficiency · 3 checks · 2 warning · 1 info

Shapes that are legal but expensive -- for reasoners, for query planners, and for anyone trying to link to the graph from outside.

EFF-001

Class hierarchy chain too deep

Warning

reasoning / traversal efficiency

A chain of 6 or more distinct rdfs:subClassOf hops exists, which increases reasoner and query-traversal cost and often signals over-specialisation.

FixFlatten the hierarchy or introduce intermediate faceted classification instead of a single deep chain.

SPARQLSHACL

sparql/efficiency/EFF-001.rq · shapes/efficiency.ttl

EFF-002

Excessive blank-node ratio

Warning

graph identity / linkability efficiency

More than 20% of the nodes in the graph are blank nodes, which hinders external linkability, caching, and stable identity across loads.

FixMint stable IRIs for entities that are referenced more than once or referenced externally; reserve blank nodes for genuinely anonymous, non-shared structure.

SPARQLSHACL

sparql/efficiency/EFF-002.rq · shapes/efficiency.ttl

EFF-003

Excessive property fan-out on a single subject

Info

data-shape efficiency

A single subject has more than 50 values for the same non-type property, a common list/collection anti-pattern that hurts query and update efficiency.

FixModel the collection as RDF List/Seq/Container, split across auxiliary resources, or move high-cardinality data out of the ontology-facing graph.

SPARQLSHACL

sparql/efficiency/EFF-003.rq · shapes/efficiency.ttl

Naming and style

style · 5 checks · 4 warning · 1 info

Conventions rather than correctness. Every finding here is defensible to ignore; what they buy is a codebase where the exceptions are visible.

STY-001

Class name not UpperCamelCase

Warning

naming convention consistency

The local name of an owl:Class does not follow UpperCamelCase (PascalCase), the de-facto convention for OWL classes.

FixRename the class local name to UpperCamelCase, e.g. 'motorVehicle' -> 'MotorVehicle'.

SPARQLSHACL

sparql/style/STY-001.rq · shapes/style.ttl

STY-002

Property name not lowerCamelCase

Warning

naming convention consistency

The local name of an owl:ObjectProperty or owl:DatatypeProperty does not follow lowerCamelCase, the de-facto convention for OWL properties.

FixRename the property local name to lowerCamelCase, e.g. 'Has_Owner' -> 'hasOwner'.

SPARQLSHACL

sparql/style/STY-002.rq · shapes/style.ttl

STY-003

Label missing a language tag

Info

internationalisation style

An rdfs:label value has no language tag, which reduces internationalisation support and can cause ambiguous display in multilingual tools.

FixAdd a language tag to the literal, e.g. "Person"@en, or add a plain xsd:string tag deliberately if language-neutrality is intended.

SPARQLSHACL

sparql/style/STY-003.rq · shapes/style.ttl

STY-004

skos:prefLabel does not match local name

Warning

naming convention consistency

A class or property's skos:prefLabel, once flattened to alphanumerics, does not match its IRI local name (also flattened), suggesting the label and IRI have drifted apart.

FixAlign the skos:prefLabel with the term's local name, or rename the term to match its intended label.

SPARQL

sparql/style/STY-004.rq

STY-005

Inconsistent local-name separator style

Warning

naming convention consistency

Class and property local names across the ontology mix more than one non-alphanumeric separator style (e.g. some hyphenated, some underscored).

FixPick a single separator convention (or none, for camelCase) and apply it consistently across all local names.

SPARQL

sparql/style/STY-005.rq

Data quality

data · 4 checks · 2 violation · 1 warning · 1 info

Checks that examine literal values and references in a populated graph, rather than the schema that governs them.

DAT-001

Literal lexical form invalid for its datatype

Violation

literal well-formedness

A literal is not valid for its declared datatype -- either its lexical form does not match the expected pattern (checked portably for xsd:date, xsd:integer and xsd:boolean) or it does not parse into that datatype's value space at all (checked for every XSD datatype the RDF parser validates, which also covers lexically well-formed impossibilities such as "2021-02-30"^^xsd:date).

FixCorrect the literal so its lexical form is valid for the declared datatype, or correct the declared datatype.

SPARQLSHACL

sparql/data/DAT-001.rq · shapes/data.ttl

DAT-002

Dangling IRI reference

Warning

referential completeness of data

An IRI is used as the object of a triple but never appears as a subject and is never used as a class anywhere in the graph, suggesting an unresolved/dangling reference.

FixAdd the missing description of the referenced resource, or fix the IRI if it was a typo.

SPARQLSHACL

sparql/data/DAT-002.rq · shapes/data.ttl

DAT-003

Duplicate literal values

Info

data redundancy

The same subject/predicate pair has the same literal value asserted more than once.

FixDe-duplicate the repeated literal values; if intentional (e.g. multiple language variants), differentiate with language tags instead of duplicating.

SPARQLSHACL

sparql/data/DAT-003.rq · shapes/data.ttl

DAT-004

gist:Magnitude without a unit of measure

Violation

quantity completeness

An entity that is a SHACL instance of gist:Magnitude (the class itself or any rdfs:subClassOf* descendant) has no gist:hasUnitOfMeasure value, or has one whose target is not a gist:UnitOfMeasure. A magnitude without a unit is not a quantity: '221.78' is not a length, and the omission stays invisible until someone tries to compare or convert two of them. Pinned to gist's exact namespace (https://w3id.org/semanticarts/ns/ontology/gist/) in both the SHACL and SPARQL formulations, which is narrower than this suite's usual local-name matching of gist terms: a graph built on a pre-gist-12 vocabulary, where the property was named gist:unitOfMeasure rather than gist:hasUnitOfMeasure, is not checked rather than checked and found wanting.

FixAdd a gist:hasUnitOfMeasure link from the magnitude to an instance typed gist:UnitOfMeasure; if the link is already present, check its target is declared a unit rather than an aspect or an untyped IRI. If the graph uses gist's pre-12 gist:unitOfMeasure spelling, widen the path in both shapes/data.ttl and sparql/data/DAT-004.rq together.

SPARQLSHACL

sparql/data/DAT-004.rq · shapes/data.ttl

Reasoning and profiles

reasoning · 10 checks · 6 violation · 4 info

Findings that only exist after inference, plus OWL2 profile membership and whatever a real DL reasoner adds beyond the always-on rule-based closure.

REA-001

Individual in two disjoint classes (post-closure)

Violation

post-closure logical consistency

After owlrl RDFS/OWL2-RL closure, an individual is a member of two classes declared owl:disjointWith each other. Unlike LOG-001 (which catches a class asserted disjoint with its own ancestor at the schema level), this is an instance-level contradiction that may only become visible once subclass/equivalence entailments are materialized.

FixEither the disjointness axiom is wrong, the individual's typing is wrong, or an upstream subclass/equivalence axiom is entailing an unintended type -- trace the closure to find which.

SPARQL

sparql/reasoning/REA-001.rq

REA-002

Asymmetric property holds in both directions

Violation

post-closure logical consistency

A property declared owl:AsymmetricProperty is asserted (or entailed) in both directions between the same pair of individuals, which is a direct contradiction of its axiom.

FixRemove the incorrect direction, or reconsider whether the property should really be declared asymmetric.

SPARQL

sparql/reasoning/REA-002.rq

REA-003

Irreflexive property asserted as a self-loop

Violation

post-closure logical consistency

A property declared owl:IrreflexiveProperty is asserted (or entailed) to relate an individual to itself, which is a direct contradiction of its axiom.

FixRemove the self-referencing triple, or reconsider whether the property should really be declared irreflexive.

SPARQL

sparql/reasoning/REA-003.rq

REA-004

Individual inferred to be a member of owl:Nothing

Violation

post-closure logical consistency

After owlrl closure, an individual is typed owl:Nothing, the empty class -- a direct sign that at least one class it is typed with is unsatisfiable given the ontology's axioms.

FixPair this suite with a real OWL2 DL reasoner (see docs/REASONING.md) to identify exactly which class is unsatisfiable and why; owlrl's rule-based closure can detect that something is wrong but not always explain the minimal contradicting axiom set.

SPARQL

sparql/reasoning/REA-004.rq

REA-010

Ontology exceeds the OWL2 EL profile

Info

OWL2 profile expressiveness

The ontology uses at least one construct not permitted in OWL2 EL (e.g. owl:unionOf, universal/cardinality restrictions beyond 0/1, or a datatype outside EL's allowed set). Informational only -- most ontologies are not intended to be EL, and this does not indicate a defect.

FixIf EL-profile reasoning (e.g. via ELK) is a goal, see docs/REASONING.md for the specific constructs found and the axioms to revise or drop.

Python

no query file — emitted directly as a ResultRow

REA-011

Ontology exceeds the OWL2 QL profile

Info

OWL2 profile expressiveness

The ontology uses at least one construct not permitted in OWL2 QL (e.g. existential restrictions in a superclass position, property chains, or cardinality restrictions beyond 0/1). Informational only.

FixIf QL-profile (first-order rewritable) query answering is a goal, see docs/REASONING.md for the specific constructs found and the axioms to revise or drop.

Python

no query file — emitted directly as a ResultRow

REA-012

Ontology exceeds the OWL2 RL profile

Info

OWL2 profile expressiveness

The ontology uses at least one construct not permitted in OWL2 RL (e.g. existentials or unions in a superclass position, or disjointness/cardinality combinations outside RL's rule-friendly forms). Informational only -- this suite's own owlrl-based reasoning backend is itself an RL-rule engine, so a non-RL ontology is exactly the case where its closure will be incomplete.

FixIf rule-based reasoning (owlrl, or any OWL2 RL engine) needs to be complete for this ontology, see docs/REASONING.md for the specific constructs found and the axioms to revise or drop.

Python

no query file — emitted directly as a ResultRow

REA-020

Ontology found inconsistent by an external DL reasoner

Violation

external DL reasoner consistency

A full OWL2 DL reasoner (HermiT/Pellet via owlready2, or another configured backend) reported the ontology (plus data, if included) as logically inconsistent -- there is no model that satisfies every axiom simultaneously.

FixConsult the reasoner's explanation (owlready2 exposes justification support) to find the minimal contradicting axiom set; owlrl's pattern-based REA-00x checks may already surface part of the same contradiction.

Python

no query file — emitted directly as a ResultRow

REA-021

Class found unsatisfiable by an external DL reasoner

Violation

external DL reasoner consistency

A full OWL2 DL reasoner determined that a named class can never have any instances given the ontology's axioms (it is equivalent to owl:Nothing), independent of whether any individual is actually asserted a member of it.

FixReview the class's superclass/disjointness/restriction axioms for a direct contradiction (e.g. disjoint with an ancestor, or a restriction requiring an unsatisfiable filler).

Python

no query file — emitted directly as a ResultRow

REA-022

External DL reasoner unavailable -- only owlrl-based checks ran

Info

external DL reasoner availability

No external OWL2 DL reasoner (owlready2 + HermiT/Pellet, or a configured ELK endpoint) was available in this environment, so only the always-on owlrl RDFS/OWL2-RL closure and pattern checks (REA-001..004, LOG-001..007) ran. These are sound but not complete for full OWL2 DL -- a class can be genuinely unsatisfiable without any of them firing.

FixInstall owlready2 plus a Java reasoner (HermiT ships with owlready2's default `sync_reasoner()`) if you need complete OWL2 DL consistency/satisfiability checking; see docs/REASONING.md.

Python

no query file — emitted directly as a ResultRow

Conformance against a separate ontology

conformance · 5 checks · 2 warning · 2 violation · 1 info

The only group that compares two things rather than examining one. A data graph, or a query-shape sketch of one, is diffed against the declarations of the ontology it is supposed to conform to.

CNF-001

Class used but not declared in the ontology

Warning

ontology conformance

A class is used with rdf:type somewhere in a graph (a real triplified data graph, or a TARQL/oxi-gen CONSTRUCT-query sketch of one) but is never declared owl:Class/rdfs:Class in the ontology it is supposed to conform to. Shared by the 'data' pipeline stage (real data vs ontology) and the 'sketch' stage (query-shape sketch vs ontology) via ontology_suite.dataquality.data_quality.check_conformance -- the finding's 'sources' tag distinguishes which graph it came from.

FixDeclare the class in the ontology, or fix the typo/undeclared-vocabulary use in the data or query.

Python

no query file — emitted directly as a ResultRow

CNF-002

Property used but not declared in the ontology

Warning

ontology conformance

A property is used somewhere in a graph (real data or a CONSTRUCT-query sketch) but is never declared as rdf:Property/owl:ObjectProperty/owl:DatatypeProperty/owl:AnnotationProperty in the ontology.

FixDeclare the property in the ontology, or fix the typo/undeclared-vocabulary use.

Python

no query file — emitted directly as a ResultRow

CNF-003

rdfs:domain violation

Violation

ontology conformance

A subject uses a property whose type (nor any ancestor) matches any of the property's declared rdfs:domain classes, in a real data graph or a CONSTRUCT-query sketch.

FixFix the subject's asserted type in the data/query, or add/relax the property's rdfs:domain in the ontology.

Python

no query file — emitted directly as a ResultRow

CNF-004

rdfs:range violation

Violation

ontology conformance

A property's value (literal datatype, or resource type) doesn't match any of the property's declared rdfs:range classes/datatypes, in a real data graph or a CONSTRUCT-query sketch.

FixFix the value's type/datatype in the data/query, or add/relax the property's rdfs:range in the ontology.

Python

no query file — emitted directly as a ResultRow

CNF-005

Ontology class never populated

Info

ontology conformance

A class the ontology declares is never used as an rdf:type in the graph under assessment. For the 'sketch' stage this commonly just means no CONSTRUCT query in the folder happens to populate that class; for a real, complete data export it may indicate a genuinely unused part of the model. Informational only.

FixExpected for a partial batch/sketch; investigate only if this class should always be populated by a complete export.

Python

no query file — emitted directly as a ResultRow

TARQL query consistency

tarql · 3 checks · 1 warning · 1 violation · 1 info

The only group that reads query source rather than a graph. A folder of TARQL queries is a program, and like any program it drifts: the same conceptual IRI ends up minted two ways in two files, and nothing about either query is invalid. These findings come from the query text, so no SPARQL or SHACL formulation of them is possible.

TQL-001

Variable bound by structurally different expressions across queries

Warning

query-set consistency

One target variable is bound by BIND in more than one query file, using expressions that differ structurally -- compared as skeletons, with every ?var reduced to ? so that feeding the same template from a differently-named column is not reported. A structural difference means the same conceptual node is minted as two different IRIs, which does not show up as an error in either query: both are valid, both produce triples, and the two IRIs simply never join. It surfaces much later as a dangling reference or a duplicate entity, a long way from the query that caused it.

FixDecide which expression is correct and use it in every file, or rename the variables so the two are not mistaken for one another.

Python

no query file — emitted directly as a ResultRow

TQL-002

Constructed-IRI variable used in CONSTRUCT but never bound

Violation

query completeness

A variable whose name follows the constructed-IRI convention (?something_IRI) appears in a CONSTRUCT template but is never bound by a BIND nor matched in the WHERE clause. The naming convention says it is built rather than read from a CSV column, so nothing will ever bind it and every triple mentioning it is silently dropped for every input row.

FixAdd the missing BIND, or correct the variable name if it should read a column directly.

Python

no query file — emitted directly as a ResultRow

TQL-003

CONSTRUCT variable not bound in the query

Info

query completeness

A variable appears in a CONSTRUCT template but is not bound by a BIND and does not appear in the WHERE clause. This is ordinarily correct rather than a defect: TARQL binds each CSV header as a variable of the same name, so most such variables are simply columns. It is reported at Info because the only way to tell a column from a typo is to read the CSV header, which is a reviewer's judgement rather than something the query text can settle.

FixCheck the variable against the CSV header. If there is no such column it is a typo, and the triple is silently dropped for every row.

Python

no query file — emitted directly as a ResultRow

A note on the closure-safe split

Four logical checks live in sparql/logical/closure-safe/ rather than beside their siblings, because the reasoning layer re-runs only that subdirectory (plus sparql/reasoning/) against the owlrl deductive closure. Both directories are discovered normally by the ordinary checks stage; the split changes only what gets re-run after inference.

The distinction is real, not organisational. LOG-001, LOG-002, LOG-004 and LOG-005 describe genuine contradictions that stay contradictions whether the triples producing them were asserted or entailed — catching those post-closure is the whole point of the reasoning pass. The others describe how the ontology was authored, and become meaningless once inference has run: LOG-003 flags a redundantly authored equivalentClass/subClassOf pair, but OWL2 RL entails that reciprocal subClassOf from every equivalentClass unconditionally. Run post-closure against a real gist-importing ontology, it fired on 59 of 59 axioms, none of them authored redundantly. LOG-006 and LOG-007 fail the same way through rdfs:subPropertyOf inheriting a superproperty's domain and range.

Adding a check

Pick the lightest of the three kinds that fits. Most checks are a graph pattern, and a graph pattern is a SPARQL check.

A portable SPARQL check

  1. Pick an id and category

    Ids follow <PREFIX>-<NNN>. In use today: STR structural, LOG logical, QUA quality, EFF efficiency, STY style, DAT data, REA reasoning, CNF conformance. A new category is fine — add it to the registry and to CATEGORY_TITLES in docs/generate_checks_md.py.

  2. Add the registry entry

    {
      "id": "STY-006",
      "category": "style",
      "metric": "short description of what is measured",
      "default_severity": "Warning",
      "title": "One-line summary",
      "description": "What condition is being flagged, in prose.",
      "remediation": "What the reader should do about it.",
      "cucumber_feature": "Naming Style",
      "cucumber_scenario": "One sentence, phrased as an expectation that should hold"
    }

    The scenario line is phrased as the expectation, not the failure — it is rendered as a Gherkin Then, so it reads as something that should be true.

  3. Write sparql/<category>/<id>.rq

    Follow the result contract above. Keep it self-contained: its own PREFIX declarations, no reliance on external state. Copy a neighbour in the same category as a starting point.

  4. Run it against a graph that should fire, and one that should not

    from rdflib import Graph
    g = Graph(); g.parse("fixture.ttl", format="turtle")
    q = open("sparql/style/STY-006.rq", encoding="utf-8").read()
    print(len(list(g.query(q).graph)))

    A query that looks right but matches nothing against a fixture that should trigger it is a bug, not a pass. This suite's own REA-001 shipped with exactly that: a FILTER(STR(?c1) < STR(?c2)) assuming owl:disjointWith gets symmetrised by reasoning, which it does not.

  5. Regenerate the docs

    python docs/generate_checks_md.py

    Nothing else to wire. The runners discover the file, the registry resolves the id, and the check appears in every table, plot and Cucumber feature automatically.

Adding a SHACL twin

Optional but recommended, since it is what earns the cross-validation signal. Put the shape in shapes/<category>.ttl. Prefer native SHACL core constraints — sh:minCount, sh:pattern, sh:or, sh:disjoint, property paths — where the check maps onto them cleanly; otherwise use sh:sparql with a sh:select mirroring the .rq file's WHERE clause:

oq:STR-003
  a sh:NodeShape ;
  rdfs:label "STR-003: property missing both domain and range" ;
  sh:severity sh:Warning ;                 # on the shape, never on the constraint
  sh:target [
    a sh:SPARQLTarget ;
    sh:select """SELECT ?this WHERE { ?this a owl:ObjectProperty }""" ;
  ] ;
  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "{$this} declares neither domain nor range." ;
    sh:select """SELECT $this WHERE { ... }""" ;
  ] .

Naming the shape exactly oq:<id> is all the tagging it needs. If one check needs several node shapes, put oq:checkId "<id>" on each instead.

The severity trap

Declare sh:severity on the shape node, never inside the nested sh:sparql [ ... ] block. SHACL defines it as a property of a shape; on the constraint it parses cleanly, is honoured by some processors and ignored by others, and pyshacl silently substitutes sh:Violation. The result is a check whose severity changes with --engine. A test fails on both the misplacement and on a severity that disagrees with the registry.

A native Python check

Reach for this only when there is genuinely no single graph to pattern-match — OWL2 profile membership, an external DL reasoner's verdict, comparing a graph against a separate ontology's declarations, or diffing two ontology versions. The conformance family is the clearest example: CNF-001 through CNF-005 cannot be SPARQL checks because they need two graphs, one supplying the declarations and one supplying the usage.

  1. Add the registry entry anyway

    Same fields as any other check. This is what lets the finding flow through the same report layer as everything else. There is deliberately no separate native-check registry.

  2. Return ResultRow objects directly

    Construct them with your check_id and category, plus whatever focus_node, path, value, message, remediation and sources apply. conformance_to_rows in dataquality/data_quality.py is the pattern to copy.

  3. Wire it into a pipeline stage

    Append the rows to the relevant stage's StageResult.rows, then regenerate the docs.

If it walks a hierarchy, walk it iteratively

Depth is set by the input — the longest rdfs:subClassOf chain, or an RDF collection's rdf:rest chain — and CPython's recursion limit is 1000 frames. Reuse ontology_suite/hierarchy.py rather than writing another recursive walk. Neither sys.setrecursionlimit (it trades a catchable error for a hard crash) nor a depth cap (any value safe under the ceiling also truncates real answers) is a fix.

Running your own panel

You do not need a checkout of the suite, or to edit anything inside site-packages, to change which checks run. checks, data and run each accept --registry, --shapes and --sparql, independently overridable and defaulting to the installed package's own resources.

# find the installed defaults and take a copy you can edit
python -c "from ontology_suite import config; print(config.PACKAGE_RESOURCES)"
cp -r <the path printed above> my-checks

# remove a check: delete its query -- discovery is a directory walk, not a manifest
rm my-checks/sparql/quality/QUA-004.rq

# add one: a registry entry plus a .rq file, exactly as above

# point the run at your copy
ontology-quality-suite checks --ontology domain.ttl   --registry my-checks/registry.json   --shapes   my-checks/shapes   --sparql   my-checks/sparql

The three flags are independent, so a project can keep the stock shapes and SPARQL while overriding only the registry — which is how you re-severitise a check for one project without touching its logic. Change default_severity in your copy and the finding keeps its id, prose and implementation while moving between Violation, Warning and Info. Since --fail-on gates the exit code on severity, that is also how you decide what breaks a build.

A minimal worked example ships in the repo: examples/acme_robotics/custom_checks/ holds a project-local registry with a single check, ACM-001, and its query — a complete illustration of the smallest thing that works.

Pitfalls worth knowing

Vocabulary you do not declare yourself

Checks that ask "is this term declared?" need to know which terms nobody declares locally. Built-in RDF, RDFS and OWL2 vocabulary is assumed axiomatically — no ontology asserts owl:Restriction a owl:Class. Omitting that exemption produced 149 false "undefined class" findings against one vehicle ontology, one per anonymous restriction. The same applies to annotation properties nobody re-declares: rdfs:label, rdfs:comment and the SKOS lexical properties. A missing skos:prefLabel exemption once flooded every SKOS-labelled taxonomy with false undeclared-property findings.

Conventions that are not rdfs:domain and rdfs:range

A structural check that hardcodes one modelling convention will fire on every ontology using a different one. STR-003 accepts gist-style domainIncludes and rangeIncludes as satisfying "has a domain and range", matched by local name rather than by a hardcoded namespace, since gist has published under more than one over the years.

Findings that only make sense before inference

Before adding a check to sparql/logical/, decide whether it survives the closure. If it describes how axioms were authored rather than what they entail, it belongs outside closure-safe/.

Nested property shapes lose their check id

pyshacl reports a property-constraint violation's sh:sourceShape as the nested blank-node property shape, not the enclosing node shape carrying the oq:checkId annotation. Id resolution walks up via sh:property to compensate. Before it did, 435 of 894 findings against a real ontology arrived with no check id, category or remediation — every native SHACL-core finding in the suite.