The "lower to the metal" you enforce a constraint, the more robust it is. It also tends to be simpler to understand and maintain higher up the chain.
For a given constraint you're trying to enforce (i.e. guarantee you want to provide), start from the top and consider each option before moving on to the next level.
For example, let's say you wanted to make sure that a custom type that can have errors must always have at least one error if it uses the error variant.
type ApplicationContext = Loading | Error ( List LoadError ) -- ...
If you enforce that with level (4) by trying to write an
elm-review rule, it will be far more complex to implement and understand the rule, and more likely to miss some cases. Instead, it's much more robust and simple to use approach (1) by changing your custom type to make impossible states impossible:
type ApplicationContext = Loading | Error ( LoadError, List LoadError ) -- ...