diff --git a/.changeset/lucky-doors-brush.md b/.changeset/lucky-doors-brush.md
new file mode 100644
index 0000000..65b6db4
--- /dev/null
+++ b/.changeset/lucky-doors-brush.md
@@ -0,0 +1,8 @@
+---
+"cube-link": patch
+---
+
+Improve validation of `meta:dimensionRelation`:
+
+1. Check that upper/lower bound has at most one `dcterms:type`
+2. Check that `meta:relatesTo` is actually a dimension
diff --git a/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl b/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl
new file mode 100644
index 0000000..d6ea241
--- /dev/null
+++ b/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl
@@ -0,0 +1,63 @@
+PREFIX dcterms:
+@prefix relation: .
+@prefix meta: .
+@prefix rdf: .
+@prefix cube: .
+@prefix observation: .
+@prefix sh: .
+@prefix xsd: .
+@prefix schema: .
+@base .
+
+ a cube:Cube ;
+ cube:observationConstraint ;
+ cube:observationSet .
+
+ cube:observation , , .
+
+ a cube:Observation ;
+ cube:observedBy ;
+ 4.9 ;
+ 0.1 ;
+.
+
+ a cube:Constraint ;
+ sh:targetClass cube:Observation ;
+ sh:closed true ;
+ sh:property
+ [
+ sh:path rdf:type ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ sh:path cube:observedBy ; ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ a cube:MeasureDimension ;
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "dimension" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ ],
+ [
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "upper confidence" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ meta:dimensionRelation
+ [
+ a relation:ConfidenceUpperBound ;
+ dcterms:type "Confidence interval", "bogus" ;
+ meta:relatesTo ;
+ ] ;
+ ];
+.
diff --git a/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl.approved.txt b/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl.approved.txt
new file mode 100644
index 0000000..2ce2feb
--- /dev/null
+++ b/test/standalone-constraint-constraint/invalid.dimensionRelation-multipleDcType.ttl.approved.txt
@@ -0,0 +1,21 @@
+@prefix sh: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xsd: .
+@prefix schema: .
+@prefix cube: .
+
+_:report a sh:ValidationReport ;
+ sh:result [
+ rdf:type sh:ValidationResult ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraintComponent sh:MaxCountConstraintComponent ;
+ sh:sourceShape [
+ sh:path ;
+ sh:maxCount 1 ;
+ ] ;
+ sh:focusNode _:b5 ;
+ sh:resultPath ;
+ sh:resultMessage "More than 1 values" ;
+ ] ;
+ sh:conforms false .
diff --git a/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl b/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl
new file mode 100644
index 0000000..7c27982
--- /dev/null
+++ b/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl
@@ -0,0 +1,62 @@
+@prefix dcterms: .
+@prefix relation: .
+@prefix meta: .
+@prefix rdf: .
+@prefix cube: .
+@prefix observation: .
+@prefix sh: .
+@prefix xsd: .
+@prefix schema: .
+@base .
+
+ a cube:Cube ;
+ cube:observationConstraint ;
+ cube:observationSet .
+
+ cube:observation , , .
+
+ a cube:Observation ;
+ cube:observedBy ;
+ 4.9 ;
+ 0.1 ;
+.
+
+ a cube:Constraint ;
+ sh:targetClass cube:Observation ;
+ sh:closed true ;
+ sh:property
+ [
+ sh:path rdf:type ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ sh:path cube:observedBy ; ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "dimension" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ ],
+ [
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "upper confidence" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ meta:dimensionRelation
+ [
+ a relation:ConfidenceUpperBound ;
+ dcterms:type "Confidence interval" ;
+ meta:relatesTo ;
+ ] ;
+ ];
+.
diff --git a/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl.approved.txt b/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl.approved.txt
new file mode 100644
index 0000000..4bac4dc
--- /dev/null
+++ b/test/standalone-constraint-constraint/invalid.dimensionRelation-relatesToNotMatching.ttl.approved.txt
@@ -0,0 +1,46 @@
+@prefix sh: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xsd: .
+@prefix schema: .
+@prefix cube: .
+
+_:report a sh:ValidationReport ;
+ sh:result [
+ rdf:type sh:ValidationResult ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraintComponent sh:NodeConstraintComponent ;
+ sh:sourceShape [
+ sh:path sh:property ;
+ sh:node ;
+ ] ;
+ sh:focusNode ;
+ sh:resultPath sh:property ;
+ sh:resultMessage "Value does not have shape " ;
+ sh:detail [
+ rdf:type sh:ValidationResult ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraintComponent sh:MinCountConstraintComponent ;
+ sh:sourceShape [
+ sh:path _:b675 ;
+ sh:class cube:Constraint ;
+ sh:minCount 1 ;
+ sh:message "value of meta:relatesTo must be a cube dimension" ;
+ ] ;
+ sh:focusNode _:b5 ;
+ sh:resultPath _:b675 ;
+ sh:resultMessage "value of meta:relatesTo must be a cube dimension" ;
+ ] ;
+ sh:value _:b4 ;
+ ] ;
+ sh:conforms false .
+
+_:b675 rdf:first ;
+ rdf:rest (
+ [
+ sh:inversePath sh:path ;
+ ]
+ [
+ sh:inversePath sh:property ;
+ ]
+ ) .
diff --git a/test/standalone-constraint-constraint/invalid.malformedList.ttl.approved.txt b/test/standalone-constraint-constraint/invalid.malformedList.ttl.approved.txt
index 97be214..e9aa921 100644
--- a/test/standalone-constraint-constraint/invalid.malformedList.ttl.approved.txt
+++ b/test/standalone-constraint-constraint/invalid.malformedList.ttl.approved.txt
@@ -10,7 +10,7 @@ _:report a sh:ValidationReport ;
rdf:type sh:ValidationResult ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:MinCountConstraintComponent ;
- sh:sourceShape _:b679 ;
+ sh:sourceShape _:b690 ;
sh:focusNode _:b6 ;
sh:resultPath rdf:rest ;
sh:resultMessage "list node needs exactly one rdf:rest" ;
@@ -41,7 +41,7 @@ _:report a sh:ValidationReport ;
rdf:type sh:ValidationResult ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:MinCountConstraintComponent ;
- sh:sourceShape _:b679 ;
+ sh:sourceShape _:b690 ;
sh:focusNode _:b6 ;
sh:resultPath rdf:rest ;
sh:resultMessage "list node needs exactly one rdf:rest" ;
@@ -60,7 +60,7 @@ _:report a sh:ValidationReport ;
] ;
sh:conforms false .
-_:b679 sh:path rdf:rest ;
+_:b690 sh:path rdf:rest ;
sh:maxCount 1 ;
sh:minCount 1 ;
sh:message "list node needs exactly one rdf:rest" .
diff --git a/test/standalone-constraint-constraint/valid.dimensionRelation.ttl b/test/standalone-constraint-constraint/valid.dimensionRelation.ttl
new file mode 100644
index 0000000..ef32bcd
--- /dev/null
+++ b/test/standalone-constraint-constraint/valid.dimensionRelation.ttl
@@ -0,0 +1,77 @@
+PREFIX dcterms:
+@prefix relation: .
+@prefix meta: .
+@prefix rdf: .
+@prefix cube: .
+@prefix observation: .
+@prefix sh: .
+@prefix xsd: .
+@prefix schema: .
+@base .
+
+ a cube:Cube ;
+ cube:observationConstraint ;
+ cube:observationSet .
+
+ cube:observation , , .
+
+ a cube:Observation ;
+ cube:observedBy ;
+ 4.9 ;
+ 0.1 ;
+ 0.15 ;
+.
+
+ a cube:Constraint ;
+ sh:targetClass cube:Observation ;
+ sh:closed true ;
+ sh:property
+ [
+ sh:path rdf:type ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ sh:path cube:observedBy ; ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:maxCount 1
+ ] ;
+ sh:property
+ [
+ a cube:MeasureDimension ;
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "dimension" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ ],
+ [
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "upper confidence" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ meta:dimensionRelation
+ [
+ a relation:ConfidenceUpperBound ;
+ dcterms:type "Confidence interval" ;
+ meta:relatesTo ;
+ ] ;
+ ],
+ [
+ sh:datatype xsd:decimal ;
+ sh:path ;
+ schema:name "lower confidence" ;
+ sh:minCount 1 ;
+ sh:maxCount 1 ;
+ meta:dimensionRelation
+ [
+ a relation:ConfidenceLowerBound ;
+ dcterms:type "Confidence interval" ;
+ meta:relatesTo ;
+ ] ;
+ ] ;
+.
diff --git a/validation/standalone-constraint-constraint.ttl b/validation/standalone-constraint-constraint.ttl
index bc3c56b..ebd84ae 100644
--- a/validation/standalone-constraint-constraint.ttl
+++ b/validation/standalone-constraint-constraint.ttl
@@ -1,3 +1,4 @@
+@prefix dcterms: .
@prefix : .
@prefix dash: .
@prefix rdf: .
@@ -8,6 +9,7 @@
@prefix cube: .
@prefix meta: .
@prefix qudt: .
+@prefix relation: .
#
# This is the bare minimal SHACL shape for validating a Cube Constraint.
@@ -179,17 +181,47 @@
.
:DimensionRelation a sh:NodeShape ;
- sh:property [
+ sh:property
+ [
sh:path meta:dimensionRelation;
- sh:property [
- sh:path meta:relatesTo;
- sh:nodeKind sh:IRI ;
- sh:minCount 1;
- sh:message "meta:dimensionRelation requires at least one meta:relatesTo";
- ] ;
+ sh:property
+ [
+ sh:path meta:relatesTo ;
+ sh:nodeKind sh:IRI ;
+ sh:minCount 1 ;
+ sh:message "meta:dimensionRelation requires at least one meta:relatesTo" ;
+ ],
+ [
+ sh:path
+ (
+ meta:relatesTo
+ [ sh:inversePath sh:path ]
+ [ sh:inversePath sh:property ]
+ ) ;
+ sh:minCount 1 ;
+ sh:class cube:Constraint ;
+ sh:message "value of meta:relatesTo must be a cube dimension" ;
+ ],
+ [
+ sh:path
+ (
+ meta:relatesTo
+ [ sh:inversePath sh:path ]
+ ) ;
+ sh:class cube:MeasureDimension ;
+ sh:message "value of meta:relatesTo must point to measure dimension " ;
+ ] ;
] ;
.
+:Confidence a sh:NodeShape ;
+ sh:targetClass relation:ConfidenceLowerBound, relation:ConfidenceUpperBound ;
+ sh:property
+ [
+ sh:path dcterms:type ;
+ sh:maxCount 1 ;
+ ] ;
+.
# Testing proper rdf:list construction