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:
Kind
Lives in
Count
Use when
Portable SPARQL
sparql/<category>/<id>.rq
42
The condition is a graph pattern over one merged graph. This is the default and covers most checks.
SHACL shape
shapes/<category>.ttl
21
Always paired with a SPARQL twin, never alone — the pair exists to cross-validate.
Native Python
reasoning/, 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.
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:
Value
Runs
Notes
both
pyshacl + portable SPARQL
Full cross-validation. The default when the native engine is absent.
sparql
portable SPARQL only
Same real findings, no drift signal, roughly 8× faster.
shacl
pyshacl only
For symmetry; strictly slower with no broader coverage.
native
native Rust SHACL engine
Optional package. Verified to find exactly what pyshacl finds.
native+sparql
native + portable SPARQL
The 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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.