diff --git a/library/Salve.hs b/library/Salve.hs
--- a/library/Salve.hs
+++ b/library/Salve.hs
@@ -30,7 +30,59 @@
 -- >>> satisfiesConstraint <$> parseConstraint ">1.2.0" <*> parseVersion "1.2.3"
 -- Just True
 
+-- * Cheat sheet
+-- | If you're coming from Cabal, you might not be familiar with npm's version
+-- range syntax. This table shows you how npm version ranges map to Cabal's
+-- version constraints.
+--
+-- > Salve           | Cabal              | Notes
+-- > -----           | -----              | -----
+-- > <1.2.3          | <1.2.3             | -
+-- > <=1.2.3         | <=1.2.3            | -
+-- > =1.2.3          | ==1.2.3            | equals sign is optional
+-- > >=1.2.3         | >=1.2.3            | -
+-- > >1.2.3          | >1.2.3             | -
+-- > 1.2.3 || >1.2.3 | ==1.2.3 || >1.2.3  | lower precedence than and
+-- > >=1.2.3 <2.0.0  | >=1.2.3 && <2.0.0  | higher precedence than or
+-- > 1.2.3 - 2.3.4   | >=1.2.3 && <=2.3.4 | inclusive ranges
+-- > 1.2.x           | ==1.2.*            | can use X or * instead of x
+-- > 1.x.x           | ==1.*              | -
+-- > x.x.x           | ==*                | same as -any
+-- > ~1.2.3          | ^>=1.2.3           | same as >=1.2.3 && <1.3.0
+-- > ^1.2.3          | >=1.2.3 && <2      | -
+-- > ^0.2.3          | >=0.2.3 && <0.3    | -
+-- > ^0.0.3          | >=0.0.3 && <0.0.4  | -
+
 -- * Rationale
+
+-- ** The PVP
+-- | Haskell's <https://pvp.haskell.org/ Package Versioning Policy> (PVP)
+-- defines three things:
+--
+-- 1.  A spec for versioning your package, which includes how version numbers
+--     look and how to encode breaking changes.
+-- 2.  A spec for constraining the versions of your dependencies, which
+--     incldues how version ranges look.
+-- 3.  A prescription for how to constrain the versions of your dependencies,
+--     which includes how the ranges of your dependencies should be.
+--
+-- By comparison, Semantic Versioning only deals with the first thing. npm's
+-- version ranges only deal with the second thing. This module deals with the
+-- first and second things but leaves the third up to you.
+--
+-- Looking at the first point, why might you want to use SemVer instead of the
+-- PVP? The PVP has many problems, as described by the
+-- <http://taylor.fausak.me/2016/12/28/problematic-versioning-policy/ Problematic versioning policy>
+-- blog post. In short, the PVP is too flexible and it's unique to Haskell,
+-- which causes unnecessary friction with developers from other languages.
+--
+-- Moving on to the second point, why should we use npm's version ranges
+-- instead of the PVP's? This is a less clear cut. The two syntaxes are broadly
+-- compatible. Really the only gains here are compatibility with a widely-used
+-- syntax and convenient shorthand for common constraints (like hyphens
+-- @1.2.3 - 2.3.4@, tildes @~1.2.3@, and carets @^1.2.3@).
+
+-- ** Other modules
 -- | There are already a few modules that provide version numbers. Why do we
 -- need another one? Let's take a look at the options.
 --
@@ -75,7 +127,7 @@
 --     -   Intentially allows weird versions.
 --     -   Does not support constraints.
 --
--- By comparison, Salve:
+-- By comparison, this module:
 --
 -- -   Does not expose constructors. Any version you create can be rendered and
 --     parsed without issue.
diff --git a/library/Salve/Internal.hs b/library/Salve/Internal.hs
--- a/library/Salve/Internal.hs
+++ b/library/Salve/Internal.hs
@@ -452,29 +452,7 @@
 -- >>> satisfiesConstraint <$> parseConstraint ">1.2.0" <*> parseVersion "1.2.3"
 -- Just True
 satisfiesConstraint :: Constraint -> Version -> Bool
-satisfiesConstraint c v = case c of
-  ConstraintOperator o w -> case o of
-    OperatorLT -> v < w
-    OperatorLE -> v <= w
-    OperatorEQ -> compare v w == EQ
-    OperatorGE -> v >= w
-    OperatorGT -> v > w
-    OperatorTilde -> (v >= w) && (v < makeVersion (versionMajor w) (versionMinor w + 1) 0 [] [])
-    OperatorCaret ->
-      let u =
-            if versionMajor w == 0
-            then if versionMinor w == 0
-            then makeVersion (versionMajor w) (versionMinor w) (versionPatch w + 1) [] []
-            else makeVersion (versionMajor w) (versionMinor w + 1) 0 [] []
-            else makeVersion (versionMajor w + 1) 0 0 [] []
-      in (v >= w) && (v < u)
-  ConstraintHyphen l r -> (l <= v) && (v <= r)
-  ConstraintWildcard w -> case w of
-    WildcardMajor -> True
-    WildcardMinor m -> (makeVersion m 0 0 [] [] <= v) && (v < makeVersion (m + 1) 0 0 [] [])
-    WildcardPatch m n -> (makeVersion m n 0 [] [] <= v) && (v < makeVersion m (n + 1) 0 [] [])
-  ConstraintAnd l r -> satisfiesConstraint l v && satisfiesConstraint r v
-  ConstraintOr l r -> satisfiesConstraint l v || satisfiesConstraint r v
+satisfiesConstraint c v = satisfiesSC (toSC c) v
 
 -- | Focuses on the major version number.
 --
@@ -800,15 +778,6 @@
 spacesP :: ReadP.ReadP ()
 spacesP = Monad.void (ReadP.munch (== ' '))
 
--- *** Helpers
-
-parse :: ReadP.ReadP a -> String -> Maybe a
-parse p s =
-  let p' = ReadP.readP_to_S p
-  in Maybe.listToMaybe (do
-    (x, "") <- p' s
-    pure x)
-
 -- ** Rendering
 
 renderPreReleases :: [PreRelease] -> String
@@ -834,3 +803,66 @@
 
 isWildcard :: Char -> Bool
 isWildcard c = (c == 'x') || (c == '*') || (c == 'X')
+
+parse :: ReadP.ReadP a -> String -> Maybe a
+parse p s =
+  let p' = ReadP.readP_to_S p
+  in Maybe.listToMaybe (do
+    (x, "") <- p' s
+    pure x)
+
+-- * Simple constraints
+-- | Simple constraints are just as expressive as 'Constraint's, but they are
+-- easier to reason about. You can think of them as the desugared version of
+-- 'Constraint's.
+
+data SimpleConstraint
+  = SCLT Version
+  | SCEQ Version
+  | SCGT Version
+  | SCAnd SimpleConstraint SimpleConstraint
+  | SCOr SimpleConstraint SimpleConstraint
+  deriving (Eq, Show)
+
+mkV :: Word -> Word -> Word -> Version
+mkV m n p = makeVersion m n p [] []
+
+satisfiesSC :: SimpleConstraint -> Version -> Bool
+satisfiesSC c v = case c of
+  SCLT u -> v < u
+  -- This uses `compare` rather than `==` to ignore build metadata.
+  SCEQ u -> compare v u == EQ
+  SCGT u -> v > u
+  SCAnd l r -> satisfiesSC l v && satisfiesSC r v
+  SCOr l r -> satisfiesSC l v || satisfiesSC r v
+
+scLE :: Version -> SimpleConstraint
+scLE v = SCOr (SCLT v) (SCEQ v)
+
+scGE :: Version -> SimpleConstraint
+scGE v = SCOr (SCGT v) (SCEQ v)
+
+toSC :: Constraint -> SimpleConstraint
+toSC c = case c of
+  ConstraintOperator o v -> case o of
+    OperatorLT -> SCLT v
+    OperatorLE -> scLE v
+    OperatorEQ -> SCEQ v
+    OperatorGE -> scGE v
+    OperatorGT -> SCGT v
+    OperatorTilde -> SCAnd
+      (scGE v)
+      (SCLT (mkV (versionMajor v) (versionMinor v + 1) 0))
+    OperatorCaret -> SCAnd
+      (scGE v)
+      (SCLT (case (versionMajor v, versionMinor v, versionPatch v) of
+        (0, 0, p) -> mkV 0 0 (p + 1)
+        (0, n, _) -> mkV 0 (n + 1) 0
+        (m, _, _) -> mkV (m + 1) 0 0))
+  ConstraintHyphen l h -> SCAnd (scGE l) (scLE h)
+  ConstraintWildcard w -> case w of
+    WildcardMajor -> scGE initialVersion
+    WildcardMinor m -> SCAnd (scGE (mkV m 0 0)) (SCLT (mkV (m + 1) 0 0))
+    WildcardPatch m n -> SCAnd (scGE (mkV m n 0)) (SCLT (mkV m (n + 1) 0))
+  ConstraintAnd l r -> SCAnd (toSC l) (toSC r)
+  ConstraintOr l r -> SCOr (toSC l) (toSC r)
diff --git a/salve.cabal b/salve.cabal
--- a/salve.cabal
+++ b/salve.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           salve
-version:        0.0.5
+version:        0.0.6
 synopsis:       Semantic version numbers and constraints.
 description:    Salve provides semantic version (SemVer) numbers and constraints (ranges).
 category:       Distribution
