SHACL rules and CDF data models
What this is
When you run validation, instance data is loaded from CDF into an RDF graph using the data model’s views (containers). The graph uses concrete view types for rdf:type, not GraphQL interface names. This affects how you should target SHACL rules.
When to use it
Use this guide when rules appear to run but produce no findings, or when targeting interfaces/shared abstractions in CDF data models.
User mental model
- Validation matches against concrete RDF types in the loaded graph.
- Interface-like concepts must be expressed as concrete targets or SPARQL target selection.
Runtime behavior
Interfaces vs concrete types
In CDF Data Modeling:
- Interfaces (e.g.
MMSINumber) are implemented by one or more views (e.g.NavigationAid,ShoreStation,SARAircraft). - Instances are stored in containers (views). Each node in the RDF graph typically gets
rdf:typeset to the view it belongs to (e.g.sb:NavigationAid), not to the interface (e.g.sb:MMSINumber).
So a rule that uses sh:targetClass sb:MMSINumber will usually not match any nodes, because no node has that type in the graph. The rule will never run and you will not get violations.
Minimal happy path
What to do
-
Target concrete views
Usesh:targetClasswith the view(s) that implement the interface (e.g.sb:NavigationAid,sb:ShoreStation,sb:SARAircraft). You can use one shape per view with the same constraint, or repeat the same property shape under each. -
Use a SPARQL-based target
Usesh:targetwith a SPARQL query that selects all nodes whose type is one of the implementing views. That way you still express “this rule applies to the interface” while matching how types appear in the graph.
Validation-time warning
When you run validation with verbose output, the runner checks whether any rule targets a class that has zero instances in the loaded graph. If so, it prints a warning, for example:
[WARNING] Some rules target a class that has no instances in the loaded graph:
- MMSINumberShape targets MMSINumber (0 instances)
This often happens when the target is a CDF *interface*: the graph uses concrete
view types. Consider targeting the concrete views that implement the interface,
or use a SPARQL-based sh:target to select all implementing types.
Use this warning to find rules that target an interface (or another class that has no instances) and then either target the concrete views or switch to a SPARQL target.
No violations doesn't mean the rule ran
If a rule produces zero violations and you expected some, check whether the target class has any instances. A shape that targets an interface (or an empty view) silently matches nothing and reports no violations — it won't error.
Not all views have data
In many CDF data models, not every view has instances. Some views may show 0 instances (e.g. ORCCertificate, RegattaEntry, Race, Regatta) because that data simply isn’t loaded yet or doesn’t exist in that space.
When a rule targets a view that has zero instances in the loaded graph:
- The rule is still evaluated, but there are no nodes of that type to validate.
- You get no violations for that rule (there is nothing to fail).
- With verbose output, you see the [WARNING] Some rules target a class that has no instances message for that target class.
It’s normal to have rules for views with no data. Leave those rules in place; they will start reporting violations once instances exist. To silence warnings for known-empty views in your environment, remove or comment out the corresponding shapes.
Troubleshooting unexpected zero violations
If a rule never produces violations when you expect it to, check three things:
- The shape targets a concrete view type, not an interface.
- Instances exist in the space and version you configured.
- Related nodes used in the rule are actually in the validation graph (see auto-load below) — not only that
auto_load_depthis high.
Run with print_output=True and verbose=True to see per-instance results and any zero-instance class warnings.
Auto-load of related instances (sh:node / reverse-direct)
Forward and reverse neighbours are not loaded into the validation RDF graph just because SPARQL mentions them. Auto-load runs only when SHACL structurally declares the target:
| Direction | Required declaration | What gets loaded |
|---|---|---|
| Forward direct relation | sh:node on the property shape, pointing at a shape with sh:targetClass |
Targets of that property |
| Reverse direct relation | neat:reverseDirectRelation with sh:node |
Sources that point at the focus node |
Important details:
auto_load_depthis a ceiling, not a trigger. Raising depth alone does not load forward direct-relation targets when nosh:nodeis present.sh:classon a property shape does not trigger auto-load. Use a stub target shape withsh:node+sh:targetClass(you can keep the same SPARQL type/existence check).- SPARQL-only patterns like
$this dm:tagRel ?tag+FILTER NOT EXISTS { ?tag a dmTag:Tag }leave?tagas a bare URI unlesssh:nodeguided a retrieve — so valid links often false-fail.
Example that matches engine behaviour today (Consistency / existence of a direct relation target):
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix dm: <http://purl.org/cognite/sp_ent_ops/SUBVManualSubseaValve/> .
@prefix dmTag: <http://purl.org/cognite/sp_ent_ops/Tag/> .
dmTag:TagExistenceShape
a sh:NodeShape ;
sh:targetClass dmTag:Tag ;
sh:name "Tag" .
dm:ValveTagRelShape
a sh:NodeShape ;
sh:targetClass dm:SUBVManualSubseaValve ;
sh:property [
sh:path dm:tagRel ;
sh:node dmTag:TagExistenceShape ; # required for auto-load
sh:sparql [
a sh:SPARQLConstraint ;
sh:select """
PREFIX dm: <http://purl.org/cognite/sp_ent_ops/SUBVManualSubseaValve/>
PREFIX dmTag: <http://purl.org/cognite/sp_ent_ops/Tag/>
SELECT $this ?tag WHERE {
$this dm:tagRel ?tag .
FILTER NOT EXISTS { ?tag a dmTag:Tag }
}
""" ;
] ;
] .
With verbose=True, validation prints a warning when SPARQL type/existence checks follow a $this relation path that has no sh:node guidance. Non-verbose runs do not print that signal (by design, to limit sync-cursor noise)—enable verbose while authoring rules.
The warning heuristic looks for $this + a simple property + FILTER NOT EXISTS { ?var a|rdf:type|… }. It does not cover every SPARQL shape (for example BIND-only paths, property paths like / or ^, or existence checks that avoid a / rdf:type). Prefer pairing SPARQL with sh:node rather than relying on the warning alone.
Best practices
- Target concrete view classes by default.
- Use SPARQL targets when one rule should apply across multiple implementing views.
- For Consistency “target exists / is typed as view X” on direct relations, always pair SPARQL with
sh:node(or reverse-direct for reverse paths). - Keep verbose output enabled during rule authoring to catch zero-instance targets and unloaded SPARQL relation paths early.
Troubleshooting
- No matches on interface target: replace interface class with implementing view classes.
- Inconsistent results across environments: verify same model version and instance spaces.
- Missing related data on forward direct relations: add
sh:node(withsh:targetClasson the target shape). Increasingauto_load_depthalone is not enough. - Reverse neighbours missing: declare
neat:reverseDirectRelation+sh:node; SPARQL alone does not load sources.
Related guides
- Atlas rule-authoring agent — package deploy for the Atlas skill that authors this
sh:node+ SPARQL pattern - Graph consistency
- Rule sources
- Run validation