diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,21 +5,47 @@
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
-## 0.2 - 2017-05-21
+## [0.3.1] - 2017-08-16
 
 ### Added
-- a multi-keyed map module
-- `decycle` function for removing npm dependency cycles
+- Functor, Foldable and Traversable instances for MKMap
 
+### Fixed
+- Remote URL parsing strips more unneeded elements
+
+## [0.3] - 2017-08-16
+
+This is a major overhaul, changing nearly every part of the API
+and the implementation!
+
+### Added
+- Support for multiple kinds of remote.
+- Heuristics for parsing git and file remotes.
+- Helpful, local error messages if the parsing goes wrong somewhere
+- A convenience function for doing all parsing steps at once
+- A pretty printer for error messages
+- Tests for all parsing logic
+- Tests for simple invariants in the multi-keyed map implementation
+
 ### Changed
-- Lockfile type is now a multi-keyed map
+- Split the code into multiple modules.
+- Rewrote the parser to have a separate AST parsing step.
 
+## [0.2] - 2017-05-21
 
+### Added
+- A multi-keyed map module.
+- `decycle` function for removing npm dependency cycles.
+
+### Changed
+- Lockfile type is now a multi-keyed map.
+
+
 ## [0.1] - 2017-04-18
 
 ### Added
-- parser for `yarn.lock` files generated by yarn
-- data types representing the yarn file
-- Lockfile type that is a simple `Map`
+- Parser for `yarn.lock` files generated by yarn.
+- Data types representing the yarn file.
+- Lockfile type that is a simple `Map`.
 
 
diff --git a/src/Data/MultiKeyedMap.hs b/src/Data/MultiKeyedMap.hs
--- a/src/Data/MultiKeyedMap.hs
+++ b/src/Data/MultiKeyedMap.hs
@@ -1,9 +1,21 @@
-{-# LANGUAGE ExistentialQuantification, NamedFieldPuns, ScopedTypeVariables #-}
+{-# LANGUAGE ExistentialQuantification, NamedFieldPuns, ScopedTypeVariables, RecordWildCards, ApplicativeDo #-}
 {-|
 Module : Data.MultiKeyedMap
-Description : A multi-keyed map.
+Description : A map with possibly multiple keys per value
 Maintainer : Profpatsch
 Stability : experimental
+
+Still very much experimental and missing lots of functions and testing.
+
+Internally, a 'MKMap' is two maps, a @keyMap@ referencing an intermediate key
+(whose type can be chosen freely and which is incremented sequentially), and
+a @valueMap@ going from intermediate key to final value.
+
+A correct implementation guarantees that
+
+(1) the internal structure can’t be corrupted by operations declared safe
+(2) adding and removing keys does not make values inaccessible
+    (thus leaking memory) and doesn’t insert unnecessary values
 -}
 module Data.MultiKeyedMap
 ( MKMap
@@ -27,7 +39,8 @@
 --
 -- Internally, we use two maps connected by an intermediate key.
 -- The intermediate key (@ik@) can be anything implementing
--- 'Ord' (for 'Map') and 'Enum' (for 'succ').
+-- 'Ord' (for 'Map'), 'Bounded' (to get the first value)
+-- and 'Enum' (for 'succ').
 data MKMap k v = forall ik. (Ord ik, Enum ik)
               => MKMap
                  { keyMap :: M.Map k ik
@@ -55,6 +68,22 @@
         -- all be reachable from the keys (TODO: add invariants)
    -- TODO: can (/=) be implemented more efficient than not.(==)?
 
+
+instance Functor (MKMap k) where
+  fmap f (MKMap{..}) = MKMap { valMap = fmap f valMap, .. }
+  {-# INLINE fmap #-}
+
+-- TODO implement all functions Data.Map also implements for Foldable
+instance Foldable (MKMap k) where
+  foldMap f (MKMap{..}) = foldMap f valMap
+  {-# INLINE foldMap #-}
+
+instance Traversable (MKMap k) where
+  traverse f (MKMap{..}) = do
+    val <- traverse f valMap
+    pure $ MKMap{ valMap=val, .. }
+  {-# INLINE traverse #-}
+
 -- | Find value at key. Partial. See 'M.!'.
 at :: (Ord k) => MKMap k v -> k -> v
 at MKMap{keyMap, valMap} k =  valMap M.! (keyMap M.! k)
@@ -70,6 +99,7 @@
         => (Proxy ik) -- ^ type of intermediate key
         -> MKMap k v -- ^ new map
 mkMKMap _ = MKMap mempty (minBound :: ik) mempty
+{-# INLINE mkMKMap #-}
 
 instance (Show k, Show v) => Show (MKMap k v) where
   showsPrec d m = Show.showString "fromList " . (showsPrec d $ toList m)
@@ -79,6 +109,7 @@
          => (Proxy ik) -- ^ type of intermediate key
          -> [([k], v)] -- ^ list of @(key, value)@
          -> MKMap k v  -- ^ new map
+
 -- TODO: it’s probably better to implement with M.fromList
 fromList p = L.foldl' (\m (ks, v) -> newVal ks v m) (mkMKMap p)
 
@@ -119,10 +150,14 @@
     upd ik = MKMap { keyMap, highestIk, valMap = M.insert ik v valMap }
 
 
--- | helper, assumes there is no such value already
---
+-- | Helper, assumes there is no such value already.
 -- Will leak space otherwise!
+--
+-- Insert every key into the keyMap, increase the intermediate counter,
+-- insert the value at new intermediate counter.
+-- Overwrites all already existing keys!
 newVal :: (Ord k) => [k] -> v -> MKMap k v -> MKMap k v
+newVal [] _ m = m
 newVal ks v MKMap{keyMap, highestIk, valMap} =
   MKMap { keyMap = L.foldl' (\m k -> M.insert k next m) keyMap ks
         , highestIk = next
diff --git a/src/Yarn/Lock.hs b/src/Yarn/Lock.hs
--- a/src/Yarn/Lock.hs
+++ b/src/Yarn/Lock.hs
@@ -1,244 +1,98 @@
-{-# LANGUAGE NoImplicitPrelude, GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE NoImplicitPrelude, LambdaCase, OverloadedStrings, RecordWildCards #-}
 {-|
 Module : Yarn.Lock
-Description : Parser & Types for yarn.lock files
+Description : High-level parser of yarn.lock files
 Maintainer : Profpatsch
 Stability : experimental
 
-The <https://yarnpkg.com/ Yarn package manager> improves on npm
-in that it writes @yarn.lock@ files that contain a complete
+The <https://yarnpkg.com/ Yarn package manager> improves on npm,
+because it writes @yarn.lock@ files that contain a complete
 version resolution of all dependencies. This way a deterministic
 deployment can be guaranteed.
-
-This module provides a parser for @yarn.lock@ files.
 -}
 module Yarn.Lock
-( Lockfile, PackageKey(..), Package(..), RemoteFile(..)
-, PackageEntry, PackageList
-, Yarn.Lock.parse
-, decycle
--- | = Parsers
-, lockfile
-, packageListToLockfile, packageList
-, packageEntry, packageKeys, packageKey, package
+( T.Lockfile
+, parseFile, parse
+-- * Errors
+, prettyLockfileError
+, LockfileError(..), PackageErrorInfo(..)
 ) where
 
-import Protolude hiding (try)
-import qualified Data.List as L
-import Data.String (String)
-import Text.Megaparsec as MP
-import Text.Megaparsec.Text
+import Protolude
 import qualified Data.Text as T
-
-import qualified Data.MultiKeyedMap as MKM
-import Data.Proxy (Proxy(..))
-
--- | Yarn lockfile.
---
--- It is a multi-keyed map (each value can be referenced by multiple keys).
--- This is achieved by using an intermediate key @ik@.
-type Lockfile = MKM.MKMap PackageKey Package
-
--- | Proxy type for our MKMap intermediate key
-lockfileIkProxy :: Proxy Int
-lockfileIkProxy = Proxy
-
--- instance Monoid Lockfile where
---    mempty = Lockfile $ MKM.mkMap (Proxy :: Proxy Int)
---    -- TODO associativity?
---    mappend = undefined
-
--- | Key that indexes package for a specific version.
-data PackageKey = PackageKey
-  { name      :: Text -- ^ package name
-  , npmSemver :: Text -- ^ semver string
-  } deriving (Show, Eq, Ord)
-
--- | The actual package with dependencies and download link.
-data Package = Package
-  { version              :: Text         -- ^ resolved, specific version
-  , resolved             :: RemoteFile   -- ^ download link w/ hash
-  , dependencies         :: [PackageKey] -- ^ list of dependencies
-  , optionalDependencies :: [PackageKey] -- ^ list of optional dependencies
-  } deriving (Eq, Show)
-
--- | A package download link.
-data RemoteFile = RemoteFile
-  { url     :: Text
-  , sha1sum :: Text }
-  deriving (Eq, Show)
-
--- | A entry as it appears in the yarn.lock representation.
-type PackageEntry = ([PackageKey], Package)
--- | Convenience alias.
-type PackageList = [PackageEntry]
-
--- | Convenience function that converts errors to Text.
---
--- The actual parsers are below.
-parse :: Text -- ^ name of source file
-      -> Text -- ^ input for parser
-      -> Either Text Lockfile
-parse src inp = first (T.pack . parseErrorPretty)
-              $ MP.parse lockfile (T.unpack src) inp
-
--- TODO: actually use somehow (apart from manual testing)
--- The yarn.lock file should resolve each packageKey exactly once.
---
--- No pkgname/semver combination should appear twice. That means
--- the lengths of the converted map and the list lists need to match.
--- prop_LockfileSameAmountOfKeys :: PackageList -> Bool
--- prop_LockfileSameAmountOfKeys pl = length (packageListToLockfile pl)
---                                    == length (concatMap fst pl)
-
-
--- | Takes a 'Lockfile' and removes dependency cycles.
---
--- Node packages often contain those and the yarn lockfile
--- does not yet eliminate them, which may lead to infinite
--- recursions.
-decycle :: Lockfile -> Lockfile
-decycle lf = goFold [] lf (MKM.keys lf)
-  -- TODO: probably rewrite with State
-  where
-    -- | fold over all package keys, passing the lockfile
-    goFold seen lf' pkeys =
-      foldl' (\lf'' pkey -> go (pkey:seen) lf'') lf' pkeys
-    -- | We get a stack of already seen packages
-    -- and filter out any dependencies we already saw.
-    go :: [PackageKey] -> Lockfile -> Lockfile
-    go seen@(we:_) lf' =
-      let ourPkg = lf' MKM.! we
-          -- old deps minus the already seen ones
-          -- TODO make handling of opt pkgs less of a duplication
-          newDeps = dependencies ourPkg L.\\ seen
-          newOptDeps = optionalDependencies ourPkg L.\\ seen
-          -- we update the pkg with the cleaned dependencies
-          lf'' = MKM.insert we (ourPkg { dependencies = newDeps
-                               , optionalDependencies = newOptDeps }) lf'
-      -- finally we do the same for all remaining deps
-      in goFold seen lf'' $ newDeps ++ newOptDeps
-    go [] _ = panic $ toS "should not happen!"
-
--- HALP, I don’t know how to parser.
--- It appears to be a more general format which somewhat resembles yaml.
--- The code below conflates the format & the semantics of yarn.lock files.
--- It should be separated sometime, to make parsing easier.
-
--- | Convenience function that applies @packageListToLockfile@.
-lockfile :: Parser Lockfile
-lockfile = packageListToLockfile <$> packageList
-
--- | The yarn.lock file is basically a hashmap with multi-keyed entries.
---
--- This should press it into our Lockfile Map.
-packageListToLockfile :: PackageList -> Lockfile
-packageListToLockfile = MKM.fromList lockfileIkProxy
-
-  -- foldl' go mempty
-  -- where go lf (keys, pkg) = foldl' (\lf' key' -> M.insert key' pkg lf') lf keys
+import qualified Data.List.NonEmpty as NE
+import qualified Text.Megaparsec as MP
+import qualified Data.Either.Validation as V
 
--- | Parse a complete yarn.lock into exaclty the same representation.
---
--- You can apply @packageListToLockfile@ to make it usable.
-packageList :: Parser PackageList
-packageList = many $ (skipMany (comment <|> eol)) *> packageEntry
-                where comment = char '#' *> manyTill anyChar eol
+import qualified Yarn.Lock.Types as T
+import qualified Yarn.Lock.File as File
+import qualified Yarn.Lock.Parse as Parse
 
--- | A single PackageEntry.
---
--- @
--- handlebars@^4.0.4:
---   version "4.0.6"
---   resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
---   dependencies:
---     async "^1.4.0"
---     optimist "^0.6.1"
---     source-map "^0.4.4"
---   optionalDependencies:
---     uglify-js "^2.6"
--- @
-packageEntry :: Parser PackageEntry
-packageEntry = (,) <$> packageKeys <*> package <?> "package entry"
+-- | Everything that can go wrong when parsing a 'Lockfile'.
+data LockfileError
+  = ParseError Text
+  -- ^ The initial parsing step failed
+  | PackageErrors (NE.NonEmpty PackageErrorInfo)
+  -- ^ a package could not be parsed from the AST
+  deriving (Show, Eq)
 
--- | The list of PackageKeys that index the same Package
---
--- @
--- align-text@^0.1.1, align-text@^0.1.3:\\n
--- @
-packageKeys :: Parser [PackageKey]
-packageKeys = sepBy1 packageKey (string ", ") <* (char ':') <* eol <?> "package keys"
+-- | Information about package parsing errors.
+data PackageErrorInfo = PackageErrorInfo
+  { srcPos :: MP.SourcePos
+  -- ^ the position of the package in the original file
+  , convErrs :: NE.NonEmpty File.ConversionError
+  -- ^ list of reasons for failure
+  } deriving (Show, Eq)
 
--- | A packageKey is @\<package-name\>\@\<semver\>@;
+-- | Convenience function, combining all parsing steps.
 --
--- If the semver contains spaces, it is also quoted with @"@.
-packageKey :: Parser PackageKey
-packageKey = label "package key" $ inString pkgKey <|> pkgKey
-  where
-    pkgKey = PackageKey
-      -- everything until the version, sep is @
-      <$> (someTextUntilSep '@' <?> "package name part of package key")
-      -- a version is anything but the , (used for seperating package keys)
-      -- or : (used to close the packageKeys line)
-      -- could be more specific (version is semver), but I’m lazy
-      <*> (someText (noneOf "\",:") <?> "semver part of package key")
-
--- | Parses the content fields of a package.
-package :: Parser Package
-package = Package
--- TODO: order shouldn’t matter, horrible indentation scheme
-  <$> (indent 2 $ key "version" stringText)
-  <*> (indent 2 $ key "resolved" remoteFile)
-  <*> (maybe [] identity <$> optional (indent 2 $ dependencyEntries "dependencies"))
-  <*> (maybe [] identity <$> optional (indent 2 $ dependencyEntries "optionalDependencies"))
-
-
--- internal parsers
+-- The resulting 'Lockfile' structure might not yet be optimal,
+-- see 'File.fromPackages'.
+parseFile :: FilePath -- ^ file to read
+          -> IO (Either LockfileError T.Lockfile)
+parseFile fp = readFile fp >>= pure . parse fp
 
--- | the “resolved”-field contains the link and the hash
-remoteFile :: Parser RemoteFile
-remoteFile = label "file link with hash" $ RemoteFile
-  <$> someTextUntilSep '#'
-  <*> stringText
+-- | For when you want to provide only the file contents.
+parse :: FilePath -- ^ name of the input file, used for the parser
+      -> Text     -- ^ content of a @yarn.lock@
+      -> Either LockfileError T.Lockfile
+parse fp = astParse fp >=> toPackages >=> toLockfile
 
--- | dependency field of a package
-dependencyEntries :: String -> Parser [PackageKey]
-dependencyEntries key' = label (key' <>" field") $ do
-  _ <- string (key' <>":") <* eol
-  -- TODO: cool indentation handling
-  some (indent 4 dep)
+-- | Pretty print a parsing error with sane default formatting.
+prettyLockfileError :: LockfileError -> Text
+prettyLockfileError = \case
+  (ParseError t) -> "Error while parsing the yarn.lock:\n"
+    <> T.unlines (indent 2 (T.lines t))
+  (PackageErrors errs) -> "Some packages could not be made sense of:\n"
+    <> T.unlines (NE.toList $ indent 2 (join $ fmap errText errs))
   where
-    -- It’s a bit like a key below, but the value of the key is not known.
-    -- Here’s where the format should get its own AST, but I’m too lazy right now.
-    dep = PackageKey <$> someTextUntilSep ' ' <*> inString stringText <* eol <?> "a dependency entry"
-
--- | A key-value pair, separated by space. The value is enclosed in "".
---
--- The given parser is used to parse the value and should not parse ".
-key :: String -> Parser a -> Parser a
-key name' val = label ("key " <> name') $
-  string name' *> char ' ' *> inString val  <* eol
-
-
--- text versions of parsers & helpers
+    indent :: Functor f => Int -> f Text -> f Text
+    indent i = fmap (T.replicate i " " <>)
+    errText (PackageErrorInfo{..}) =
+      (pure $ "Package at " <> (toS $ MP.sourcePosPretty srcPos) <> ":")
+      <> indent 2 (fmap convErrText convErrs)
+    convErrText = \case
+      (File.MissingField t) -> "Field " <> qu t <> " is missing."
+      (File.WrongType{..})  -> "Field " <> qu fieldName
+                               <> " should be of type " <> fieldType <> "."
+      (File.UnknownRemoteType) -> "We don’t know this remote type."
+    qu t = "\"" <> t <> "\""
 
-someText :: Parser Char -> Parser Text
-someText c = T.pack <$> some c
+-- helpers
+astParse :: FilePath -> Text -> Either LockfileError [Parse.Package]
+astParse fp = first (ParseError . toS . MP.parseErrorPretty)
+                . MP.parse Parse.packageList fp
 
--- | parse everything as inside a string
--- TODO: this breaks the 'between' abstraction, can it be avoided somehow?
-inString :: Parser a -> Parser a
-inString = between (char '"') (char '"')
+toPackages :: [Parse.Package] -> Either LockfileError [T.Keyed T.Package]
+toPackages = first PackageErrors . V.validationToEither
+                . traverse validatePackage
 
--- | function to annotate text inside strings (which should never parse ")
---   symptom of the broken 'between' abstraction
-stringText :: Parser Text
-stringText = someText (noneOf "\"") <?> "non-empty text without \""
+validatePackage :: Parse.Package
+                -> V.Validation (NE.NonEmpty PackageErrorInfo) (T.Keyed T.Package)
+validatePackage (T.Keyed keys (pos, fields)) = V.eitherToValidation
+  $ bimap (pure . PackageErrorInfo pos) (T.Keyed keys)
+    $ File.astToPackage fields
 
--- | parse some text until seperator is reached
-someTextUntilSep :: Char -> Parser Text
-someTextUntilSep sep = T.pack <$> someTill anyChar (char sep)
+toLockfile :: [T.Keyed T.Package] -> Either LockfileError T.Lockfile
+toLockfile = pure . File.fromPackages
 
--- | intend by @i@ spaces
-indent :: Int -> Parser a -> Parser a
-indent i p = try $ count i (char ' ') *> p
diff --git a/src/Yarn/Lock/File.hs b/src/Yarn/Lock/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Yarn/Lock/File.hs
@@ -0,0 +1,154 @@
+{-|
+Module : Yarn.Lock.File
+Description : Convert AST to semantic data structures
+Maintainer : Profpatsch
+Stability : experimental
+
+After parsing yarn.lock files in 'Yarn.Lock.Parse',
+you want to convert the AST to something with more information
+and ultimately get a 'T.Lockfile'.
+
+@yarn.lock@ files don’t follow a structured approach
+(like for example sum types), so information like e.g.
+the remote type have to be inferred frome AST values.
+-}
+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, ApplicativeDo, RecordWildCards, NamedFieldPuns #-}
+module Yarn.Lock.File
+( fromPackages
+, astToPackage
+-- * Errors
+, ConversionError(..)
+) where
+
+import Protolude
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Map.Strict as M
+import qualified Data.Text as Text
+import qualified Data.Either.Validation as V
+
+import qualified Yarn.Lock.Parse as Parse
+import qualified Yarn.Lock.Types as T
+import qualified Data.MultiKeyedMap as MKM
+
+-- | Press a list of packages into the lockfile structure.
+--
+-- It’s a dumb conversion, you should probably apply
+-- the 'Yarn.Lock.Helpers.decycle' function afterwards.
+fromPackages :: [T.Keyed T.Package] -> T.Lockfile
+fromPackages = MKM.fromList T.lockfileIkProxy
+             . fmap (\(T.Keyed ks p) -> (ks, p))
+
+-- | Possible errors when converting from AST.
+data ConversionError
+  = MissingField Text
+  -- ^ field is missing
+  | WrongType { fieldName :: Text, fieldType :: Text }
+  -- ^ this field has the wrong type
+  | UnknownRemoteType
+  -- ^ the remote (e.g. git, tar archive) could not be determined
+  deriving (Show, Eq)
+
+-- | Something that can parse the value of a field into type @a@.
+data FieldParser a = FieldParser
+  { parseField :: Either Text Parse.PackageFields -> Maybe a
+    -- ^ the parsing function (Left is a simple field, Right a nested one)
+  , parserName :: Text
+    -- ^ name of this parser (for type errors)
+  }
+
+type Val = V.Validation (NE.NonEmpty ConversionError)
+
+-- | Parse an AST 'PackageFields' to a 'T.Package', which has
+-- the needed fields resolved.
+astToPackage :: Parse.PackageFields
+             -> Either (NE.NonEmpty ConversionError) T.Package
+astToPackage = V.validationToEither . validate
+  where
+    validate :: Parse.PackageFields -> Val T.Package
+    validate fs = do
+      version              <- getField text "version" fs
+      remote               <- checkRemote fs
+      dependencies         <- getFieldOpt keylist "dependencies" fs
+      optionalDependencies <- getFieldOpt keylist "optionalDependencies" fs
+      pure $ T.Package{..}
+
+    -- | Parse a field from a 'PackageFields'.
+    getField :: FieldParser a -> Text -> Parse.PackageFields -> Val a
+    getField = getFieldImpl Nothing
+    -- | Parse an optional field and insert the empty monoid value
+    getFieldOpt :: Monoid a => FieldParser a -> Text -> Parse.PackageFields -> Val a
+    getFieldOpt = getFieldImpl (Just mempty)
+
+    getFieldImpl :: Maybe a -> FieldParser a -> Text -> Parse.PackageFields -> Val a
+    getFieldImpl mopt typeParser fieldName (Parse.PackageFields m)=
+      first pure $ V.eitherToValidation $ do
+        case M.lookup fieldName m of
+          Nothing -> case mopt of
+            Just opt -> Right opt
+            Nothing  -> Left $ MissingField fieldName
+          Just val -> note
+            (WrongType { fieldName, fieldType = parserName typeParser })
+            $ parseField typeParser val
+
+    -- | Parse a simple field to type 'Text'.
+    text :: FieldParser Text
+    text = FieldParser { parseField = either Just (const Nothing)
+                       , parserName = "text" }
+
+    -- | Parse a field nested one level to a list of 'PackageKey's.
+    keylist :: FieldParser [T.PackageKey]
+    keylist = FieldParser
+      { parserName = "list of package keys"
+      , parseField = either (const Nothing)
+             (\(Parse.PackageFields inner) ->
+                  for (M.toList inner) $ \(k, v) -> do
+                    npmVersionSpec <- parseField text v
+                    pure $ T.PackageKey { T.name = k, ..}) }
+
+    -- | Appling heuristics to the field contents to find the
+    -- correct remote type.
+    checkRemote :: Parse.PackageFields -> Val T.Remote
+    checkRemote fs =
+      -- any error is replaced by the generic remote error
+      mToV (pure UnknownRemoteType)
+        -- implementing the heuristics of searching for types;
+        -- it should of course not lead to false positives
+        -- see tests/TestLock.hs
+        $ checkGit <|> checkFile
+      where
+        mToV :: e -> Maybe a -> V.Validation e a
+        mToV err = V.eitherToValidation . note err
+        vToM :: Val a -> Maybe a
+        vToM = hush . V.validationToEither
+
+        -- | "https://blafoo.com/a/b#alonghash"
+        --   -> ("https://blafoo.com/a/b", "alonghash")
+        -- we assume the # can only occur exactly once
+        findUrlHash :: Text -> (Text, Maybe Text)
+        findUrlHash url = case Text.splitOn "#" url of
+          [url']       -> (url', Nothing)
+          [url', ""]   -> (url', Nothing)
+          [url', hash] -> (url', Just hash)
+          _           -> panic "checkRemote: # should only appear exactly once!"
+
+        checkGit = do
+          resolved <- vToM $ getField text "resolved" fs
+          -- either in uid field or after the hash in the “resolved” URL
+          (repo, gitRev) <- do
+            let (repo', mayHash) = findUrlHash resolved
+            hash <- vToM (getField text "uid" fs)
+              <|> if any (`Text.isPrefixOf` resolved) ["git+", "git://"]
+                  then mayHash else Nothing
+            pure (repo', hash)
+          pure $ T.GitRemote
+            { T.gitRepoUrl = noPrefix "git+" repo , .. }
+
+        -- | ensure the prefix is removed
+        noPrefix :: Text -> Text -> Text
+        noPrefix pref hay = maybe hay identity $ Text.stripPrefix pref hay
+
+        checkFile = do
+          resolved <- vToM (getField text "resolved" fs)
+          let (fileUrl, mayFileSha1) = findUrlHash resolved
+          fileSha1 <- mayFileSha1
+          pure $ T.FileRemote{..}
diff --git a/src/Yarn/Lock/Helpers.hs b/src/Yarn/Lock/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/Yarn/Lock/Helpers.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-|
+Module : Yarn.Lock.Helpers
+Description : Helpers for modifying Lockfiles
+Maintainer : Profpatsch
+Stability : experimental
+
+Freshly parsed 'Lockfile's are often not directly usable
+e.g. because they still can contain cycles. This module
+provides helpers for modifying them.
+-}
+module Yarn.Lock.Helpers
+( decycle
+) where
+
+import Protolude
+import qualified Data.List as L
+
+import qualified Data.MultiKeyedMap as MKM
+
+import Yarn.Lock.Types
+
+
+-- | Takes a 'Lockfile' and removes dependency cycles.
+--
+-- Node packages often contain those and the yarn lockfile
+-- does not yet eliminate them, which may lead to infinite
+-- recursions.
+decycle :: Lockfile -> Lockfile
+decycle lf = goFold [] lf (MKM.keys lf)
+  -- TODO: probably rewrite with State
+  where
+    -- | fold over all package keys, passing the lockfile
+    goFold seen lf' pkeys =
+      foldl' (\lf'' pkey -> go (pkey:seen) lf'') lf' pkeys
+    -- | We get a stack of already seen packages
+    -- and filter out any dependencies we already saw.
+    go :: [PackageKey] -> Lockfile -> Lockfile
+    go seen@(we:_) lf' =
+      let ourPkg = lf' MKM.! we
+          -- old deps minus the already seen ones
+          -- TODO make handling of opt pkgs less of a duplication
+          newDeps = dependencies ourPkg L.\\ seen
+          newOptDeps = optionalDependencies ourPkg L.\\ seen
+          -- we update the pkg with the cleaned dependencies
+          lf'' = MKM.insert we (ourPkg { dependencies = newDeps
+                               , optionalDependencies = newOptDeps }) lf'
+      -- finally we do the same for all remaining deps
+      in goFold seen lf'' $ newDeps ++ newOptDeps
+    go [] _ = panic $ toS "should not happen!"
diff --git a/src/Yarn/Lock/Parse.hs b/src/Yarn/Lock/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Yarn/Lock/Parse.hs
@@ -0,0 +1,194 @@
+{-# LANGUAGE NoImplicitPrelude, GeneralizedNewtypeDeriving #-}
+{-|
+Module : Yarn.Lock.Parse
+Description : Parser for yarn.lock files
+Maintainer : Profpatsch
+Stability : experimental
+
+This module provides a parser for the AST of @yarn.lock@ files.
+-}
+module Yarn.Lock.Parse
+( PackageFields(..), Package
+-- * Parsing
+-- ** Re-export
+, Parser
+-- ** Parsers
+, packageList
+, packageEntry
+-- = internal parsers
+, field, packageKeys, packageKey
+) where
+
+import Protolude hiding (try)
+import qualified Data.Char as Ch
+import qualified Data.Text as Text
+import qualified Data.Map.Strict as M
+import Text.Megaparsec as MP hiding (space)
+import Text.Megaparsec.Text
+import qualified Text.Megaparsec.Lexer as MPL
+
+-- import qualified Data.MultiKeyedMap as MKM
+-- import Data.Proxy (Proxy(..))
+
+import qualified Yarn.Lock.Types as T
+
+
+-- | The @yarn.lock@ format doesn’t specifically include a fixed scheme,
+-- it’s just an unnecessary custom version of a list of fields.
+--
+-- An field can either be a string or more fields w/ deeper indentation.
+--
+-- The actual conversion to semantic structures needs to be done afterwards.
+newtype PackageFields = PackageFields (Map Text (Either Text PackageFields))
+  deriving (Show, Eq, Monoid)
+
+-- | A parsed 'Package' AST has one or more keys, a position in the original files
+-- and a collection of fields.
+type Package = T.Keyed (SourcePos, PackageFields)
+
+
+-- | Parse a complete yarn.lock into an abstract syntax tree,
+-- keeping the source positions of each package entry.
+packageList :: Parser [Package]
+packageList = many $ (skipMany (comment <|> eol)) *> packageEntry
+                where comment = char '#' *> manyTill anyChar eol
+
+-- | A single Package.
+--
+-- Example:
+--
+-- @
+-- handlebars@^4.0.4:
+--   version "4.0.6"
+--   resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
+--   dependencies:
+--     async "^1.4.0"
+--     optimist "^0.6.1"
+--     source-map "^0.4.4"
+--   optionalDependencies:
+--     uglify-js "^2.6"
+-- @
+packageEntry :: Parser (T.Keyed (SourcePos, PackageFields))
+packageEntry = label "package entry" $ do
+  pos <- getPosition
+  -- A package entry is a non-indented
+  (keys, pkgs) <- nonIndented
+            -- block that has a header of package keys
+            -- and an indented part that contains fields
+            $ indentedFieldsWithHeader packageKeys
+  pure $ T.Keyed keys (pos, pkgs)
+
+-- | The list of PackageKeys that index the same Package
+--
+-- @
+-- align-text@^0.1.1, align-text@^0.1.3:\\n
+-- @
+packageKeys :: Parser [T.PackageKey]
+packageKeys = label "package keys" $ do
+  firstEls <- many (try $ lexeme $ packageKey ":," <* char ',')
+  lastEl   <-                      packageKey ":"  <* char ':'
+  pure $ firstEls ++ [lastEl]
+
+-- | A packageKey is @\<package-name\>\@\<semver\>@;
+--
+-- If the semver contains spaces, it is also quoted with @"@.
+packageKey :: [Char] -> Parser T.PackageKey
+packageKey separators = inString (pkgKey "\"")
+         -- if no string delimiters is used we need to check for the separators
+         -- this file format is shit :<
+         <|> pkgKey separators
+         <?> "package key"
+  where
+    pkgKey valueChars = do
+      pkgName <- someTextOf (noneOf "@") <?> "package name part of package key"
+      _ <- char '@'
+      semver <- (someTextOf (noneOf valueChars))
+                <|> (pure Text.empty <?> "an empty semver")
+                <?> "semver part of package key"
+      pure $ T.PackageKey pkgName semver
+
+
+-- | Either a simple or a nested field.
+field :: Parser (Text, Either Text PackageFields)
+field = try nested <|> simple <?> "field"
+  where
+    simple = fmap Left <$> simpleField
+    nested = fmap Right <$> nestedField
+
+-- | A key-value pair, separated by space. The value is enclosed in "".
+-- Returns key and value.
+simpleField :: Parser (Text, Text)
+simpleField = (,) <$> lexeme symbolChars
+                  -- valueChars may be in Strings or maybe not >:
+                  -- this file format is absolute garbage
+                  <*> (strValueChars <|> valueChars)
+                  <?> "simple field"
+  where
+    valueChars, strValueChars :: Parser Text
+    valueChars = someTextOf (noneOf "\n\r\"")
+    strValueChars = inString $ valueChars
+      -- as with packageKey semvers, this can be empty
+      <|> (pure Text.empty <?> "an empty value field")
+
+-- | Similar to a @simpleField@, but instead of a string
+-- we get another block with deeper indentation.
+nestedField :: Parser (Text, PackageFields)
+nestedField = label "nested field" $
+  indentedFieldsWithHeader (symbolChars <* char ':')
+
+
+-- internal parsers
+
+-- | There are two kinds of indented blocks:
+-- One where the header is the package
+-- and one where the header is already a package field key.
+indentedFieldsWithHeader :: Parser a -> Parser (a, PackageFields)
+indentedFieldsWithHeader header = indentBlock $ do
+    -- … block that has a header of package keys
+    hdr <- header
+    -- … and an indented part that contains fields
+    pure $ MPL.IndentSome Nothing
+      (\fields -> pure (hdr, toPfs fields)) field
+  where
+    toPfs :: [(Text, Either Text PackageFields)] -> PackageFields
+    toPfs = PackageFields . M.fromList
+
+-- | Characters allowed in key symbols.
+-- 
+-- TODO: those are partly npm package names, so check the allowed symbols, too.
+--
+-- Update: npm doesn’t specify the package name format, at all.
+-- Apart from the length.
+symbolChars :: Parser Text
+symbolChars = label "key symbol" $ someTextOf $ satisfy
+  (\c -> Ch.isAscii c &&
+     (Ch.isLower c || Ch.isUpper c || Ch.isNumber c || c `elem` "-_."))
+
+
+-- text versions of parsers & helpers
+
+someTextOf :: Parser Char -> Parser Text
+someTextOf c = Text.pack <$> some c
+
+-- | parse everything as inside a string
+inString :: Parser a -> Parser a
+inString = between (char '"') (char '"')
+
+-- lexers
+
+-- | Parse whitespace.
+space :: Parser ()
+space = MPL.space (void MP.spaceChar)
+                  (MPL.skipLineComment "# ")
+                  (void $ satisfy (const False))
+
+-- | Parse a lexeme.
+lexeme :: Parser a -> Parser a
+lexeme = MPL.lexeme space
+
+-- | Ensure parser is not indented.
+nonIndented :: Parser a -> Parser a
+nonIndented = MPL.nonIndented space
+indentBlock :: ParsecT Dec Text Identity (MPL.IndentOpt (ParsecT Dec Text Identity) a b)
+            -> ParsecT Dec Text Identity a
+indentBlock = MPL.indentBlock space
diff --git a/src/Yarn/Lock/Types.hs b/src/Yarn/Lock/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Yarn/Lock/Types.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-|
+Module : Yarn.Lock.Types
+Description : Types for yarn.lock files
+Maintainer : Profpatsch
+Stability : experimental
+-}
+module Yarn.Lock.Types where
+
+import Protolude hiding (try)
+import qualified Data.MultiKeyedMap as MKM
+
+-- | Yarn lockfile.
+--
+-- It is a multi-keyed map (each value can be referenced by multiple keys).
+-- This is achieved by using an intermediate key @ik@.
+--
+-- Attention: Might be changed to a newtype in a future release.
+type Lockfile = MKM.MKMap PackageKey Package
+-- TODO
+-- newtype Lockfile = Lockfile (MKM.MKMap PackageKey Package)
+
+-- | Proxy type for our MKMap intermediate key
+lockfileIkProxy :: Proxy Int
+lockfileIkProxy = Proxy
+
+-- | Key that indexes package for a specific version.
+data PackageKey = PackageKey
+  { name           :: Text -- ^ package name
+  , npmVersionSpec :: Text
+  -- ^ tring that specifies the version of a package;
+  -- sometimes a npm semver, sometimes an arbitrary string
+  } deriving (Show, Eq, Ord)
+
+-- | Something with a list of 'PackageKey's pointing to it.
+data Keyed a = Keyed [PackageKey] a
+  deriving (Show, Eq, Ord, Functor)
+
+-- | The actual npm package with dependencies and a way to download.
+data Package = Package
+  { version              :: Text         -- ^ resolved, specific version
+  , remote               :: Remote
+  , dependencies         :: [PackageKey] -- ^ list of dependencies
+  , optionalDependencies :: [PackageKey] -- ^ list of optional dependencies
+  } deriving (Eq, Show)
+
+-- | Information on where to download the package.
+data Remote
+  = FileRemote
+  { fileUrl     :: Text -- ^ URL to a remote file
+  , fileSha1    :: Text -- ^ sha1 hash of the file (attached to the link)
+
+  }
+  | GitRemote
+  { gitRepoUrl  :: Text -- ^ valid git remote URL
+  , gitRev      :: Text -- ^ git tree-ish (commit, branch, &c.)
+  } deriving (Eq, Show)
diff --git a/tests/Test.hs b/tests/Test.hs
--- a/tests/Test.hs
+++ b/tests/Test.hs
@@ -1,6 +1,13 @@
 module Main where
 
 import Test.Tasty
-import qualified TestLock as TL
+import qualified TestParse as Parse
+import qualified TestFile as File
+import qualified TestMultiKeyedMap as MKM
 
-main = defaultMain TL.tests
+main :: IO ()
+main = defaultMain $ testGroup "tests"
+  [ Parse.tests
+  , File.tests
+  , MKM.tests
+  ]
diff --git a/tests/TestFile.hs b/tests/TestFile.hs
new file mode 100644
--- /dev/null
+++ b/tests/TestFile.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell, NamedFieldPuns, ViewPatterns, NoImplicitPrelude, LambdaCase, RecordWildCards #-}
+module TestFile (tests) where
+
+import Protolude
+import qualified Data.List.NonEmpty as NE
+import Test.Tasty (TestTree)
+import Test.Tasty.TH
+import Test.Tasty.HUnit
+import qualified Data.Map.Strict as M
+
+import qualified Yarn.Lock.Types as T
+import qualified Yarn.Lock.File as File
+import qualified Yarn.Lock.Parse as Parse
+
+-- TODO: actually use somehow (apart from manual testing)
+-- The yarn.lock file should resolve each packageKey exactly once.
+--
+-- No pkgname/semver combination should appear twice. That means
+-- the lengths of the converted map and the list lists need to match.
+-- prop_LockfileSameAmountOfKeys :: [Package] -> Bool
+-- prop_LockfileSameAmountOfKeys pl = length (packageListToLockfile pl)
+--                                    == length (concatMap fst pl)
+
+emptyAst :: [(Text, Either Text Parse.PackageFields)] -> Parse.PackageFields
+emptyAst = Parse.PackageFields . M.fromList
+
+minimalAst :: [(Text, Either Text Parse.PackageFields)] -> Parse.PackageFields
+minimalAst = emptyAst . ([("version", Left "0.3")] <>)
+
+case_gitRemote :: Assertion
+case_gitRemote = do
+  let ref = "abcthisisaref"
+      ast link_ hasUid = minimalAst $
+          [ ("resolved", Left link_) ]
+          <> hasUid `orEmpty` ("uid", Left ref)
+  let gitRemIs parsed (url', ref') = parsed
+        <&> T.remote >>= \case
+          T.GitRemote{..} -> do
+            assertEqual "url url" url' gitRepoUrl
+            assertEqual "url ref" ref' gitRev
+          a -> assertFailure ("should be GitRemote, is " <> show a)
+  let url1 = "git://github.com/bla"
+  astToPackageSuccess (ast (url1 <> "#" <> ref) False)
+    `gitRemIs` (url1, ref)
+  let url2 = "https://github.com/bla"
+  astToPackageSuccess (ast ("git+" <> url2) True)
+    `gitRemIs` (url2, ref)
+
+case_fileRemote :: Assertion
+case_fileRemote = do
+  let sha = "helloimref"
+      good = minimalAst $
+          [ ("resolved", Left $ "https://gnu.org/stallmanstoe#" <> sha) ]
+      bad = minimalAst [ ("resolved", Left "https://nohash") ]
+  astToPackageSuccess good
+    <&> T.remote >>= \case
+      T.FileRemote{..} -> assertEqual "file sha" sha fileSha1
+      a -> assertFailure ("should be FileRemote, is " <> show a)
+  astToPackageFailureWith (pure File.UnknownRemoteType) bad
+
+case_missingField :: Assertion
+case_missingField = do
+  astToPackageFailureWith
+    (File.MissingField "version"
+     NE.:| [File.UnknownRemoteType]) $ emptyAst []
+
+-- will be in protolude soon
+infixl 4 <&>
+(<&>) :: Functor f => f a -> (a -> b) -> f b
+(<&>) = flip fmap
+
+astToPackageSuccess :: Parse.PackageFields -> IO T.Package
+astToPackageSuccess ast = case File.astToPackage ast of
+  (Left errs) -> do
+     assertFailure ("should have succeded, but:\n" <> show errs)
+     panic "not reached"
+  (Right pkg) -> pure pkg
+
+astToPackageFailureWith :: (NE.NonEmpty File.ConversionError)
+                        -> Parse.PackageFields -> IO ()
+astToPackageFailureWith errs ast = case File.astToPackage ast of
+  (Right _) -> assertFailure "should have failed"
+  (Left actual) -> assertEqual "errors should be the same" errs actual
+
+--TODO
+{-
+data Keys = Keys { a, b, c, y, z :: PackageKey }
+keys :: Keys
+keys = Keys (pk "a") (pk "b") (pk "c") (pk "y") (pk "z")
+  where pk n = PackageKey n "0.1"
+
+data LFs = LFs
+  { lfNormal, lfEmpty, lfCycle, lfDecycled
+  , lfComplex, lfComplexD :: Lockfile }
+-- | Example lockfiles for tests.
+-- These are put into scope in tests by use of @NamedFieldPuns@.
+lfs :: LFs
+lfs = LFs
+  { lfNormal = (tlf [pkg' a [b, c], pkg' b [c], pkg' c []])
+  , lfEmpty  = (tlf [])
+  , lfCycle    = (tlf [pkg' a [b, c], pkg' b [a, c], pkg' c [c]])
+  , lfDecycled = (tlf [pkg' a [b, c], pkg' b [   c], pkg' c [ ]])
+  , lfComplex  = (tlf [pkg [a, z] [a, c], pkg [c, y] [c, a, z]])
+  -- Hm, this test is implementation dependent. But the cycles get removed.
+  , lfComplexD = (tlf [pkg [a, z] [    ], pkg [c, y] [      z]])
+  }
+  where pkg keys_ deps = (keys_, Package "0.1" (RemoteFile "" "") deps [])
+        pkg' key = pkg [key]
+        tlf = packageListToLockfile
+        Keys{a,b,c,y,z} = keys
+
+-- | Test for the 'decycle' method.
+case_decycle :: Assertion
+case_decycle = do
+  -- print lfCycle
+  lfDecycled @=? (decycle lfCycle)
+  lfComplexD @=? (decycle lfComplex)
+  where LFs{lfCycle, lfDecycled, lfComplex, lfComplexD} = lfs
+
+type PkgMap = Map PackageKey Package
+
+-- | A lockfile is basically a flat version of a recursive dependency structure.
+-- 'Built' resembles the recursive version of said flat structure.
+data Built = Built PackageKey [Built] deriving (Eq)
+instance Show Built where
+  show (Built k b) = show $ printBuild b
+    where printBuild b' = Pr.list
+            [Pr.tupled [Pr.text . toS $ name k, printBuild b']]
+
+buildFromMap :: PkgMap -> [Built]
+buildFromMap m = map go $ M.keys m
+  where
+    go :: PackageKey -> Built
+    go pk = Built pk $ map go (dependencies $ m M.! pk)
+
+-- | Checks if the flat lockfile builds a correct recursive structure.
+case_built :: Assertion
+case_built = do
+  let
+    LFs{lfNormal} = lfs
+    Keys{a,b,c} = keys
+    bl = Built
+    ble p = Built p []
+  buildFromMap (flattenKeys lfNormal)
+    @?= [ bl a [bl b [ble c], ble c]
+        , bl b [ble c]
+        , ble c]
+
+
+
+-}
+
+tests :: TestTree
+tests = $(testGroupGenerator)
+
diff --git a/tests/TestLock.hs b/tests/TestLock.hs
deleted file mode 100644
--- a/tests/TestLock.hs
+++ /dev/null
@@ -1,76 +0,0 @@
-{-# LANGUAGE OverloadedStrings, TemplateHaskell, NamedFieldPuns, ViewPatterns, NoImplicitPrelude #-}
-module TestLock (tests) where
-
-import Protolude
-import Yarn.Lock
-import Data.MultiKeyedMap hiding (keys)
-import Test.Tasty (TestTree)
-import Test.Tasty.TH
-import Test.Tasty.HUnit
-import qualified Text.Show as S
-import qualified Text.PrettyPrint.ANSI.Leijen as Pr
-import qualified Data.Map.Strict as M
-
-data Keys = Keys { a, b, c, y, z :: PackageKey }
-keys :: Keys
-keys = Keys (pk "a") (pk "b") (pk "c") (pk "y") (pk "z")
-  where pk n = PackageKey n "0.1"
-
-data LFs = LFs
-  { lfNormal, lfEmpty, lfCycle, lfDecycled
-  , lfComplex, lfComplexD :: Lockfile }
-lfs :: LFs
-lfs = LFs
-  { lfNormal = (tlf [pkg' a [b, c], pkg' b [c], pkg' c []])
-  , lfEmpty  = (tlf [])
-  , lfCycle    = (tlf [pkg' a [b, c], pkg' b [a, c], pkg' c [c]])
-  , lfDecycled = (tlf [pkg' a [b, c], pkg' b [   c], pkg' c [ ]])
-  , lfComplex  = (tlf [pkg [a, z] [a, c], pkg [c, y] [c, a, z]])
-  -- Hm, this test is implementation dependent. But the cycles get removed.
-  , lfComplexD = (tlf [pkg [a, z] [    ], pkg [c, y] [      z]])
-  }
-  where pkg keys_ deps = (keys_, Package "0.1" (RemoteFile "" "") deps [])
-        pkg' key = pkg [key]
-        tlf = packageListToLockfile
-        Keys{a,b,c,y,z} = keys
-
-case_decycle :: Assertion
-case_decycle = do
-  -- print lfCycle
-  lfDecycled @=? (decycle lfCycle)
-  lfComplexD @=? (decycle lfComplex)
-  where LFs{lfCycle, lfDecycled, lfComplex, lfComplexD} = lfs
-  
-
-type PkgMap = Map PackageKey Package
-
-data Built = Built PackageKey [Built] deriving (Eq)
-instance Show Built where
-  show (Built k b) = show $ printBuild b
-    where printBuild b' = Pr.list
-            [Pr.tupled [Pr.text . toS $ name k, printBuild b']]
-
-buildFromMap :: PkgMap -> [Built]
-buildFromMap m = map go $ M.keys m
-  where
-    go :: PackageKey -> Built
-    go pk = Built pk $ map go (dependencies $ m M.! pk)
-
--- lfBuilt :: Lockfile -> [Built]
--- lfBuilt = buildFromMap . flattenKeys
-
-case_built :: Assertion
-case_built = do
-  let
-    LFs{lfNormal} = lfs
-    Keys{a,b,c} = keys
-    bl = Built
-    ble p = Built p []
-  buildFromMap (flattenKeys lfNormal)
-    @?= [ bl a [bl b [ble c], ble c]
-        , bl b [ble c]
-        , ble c]
-
-
-tests :: TestTree
-tests = $(testGroupGenerator)
diff --git a/tests/TestMultiKeyedMap.hs b/tests/TestMultiKeyedMap.hs
new file mode 100644
--- /dev/null
+++ b/tests/TestMultiKeyedMap.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NamedFieldPuns, ViewPatterns, NoImplicitPrelude #-}
+module TestMultiKeyedMap (tests) where
+
+import Protolude
+import qualified Data.List as List
+import Test.Tasty (TestTree)
+import Test.Tasty.TH
+import Test.Tasty.QuickCheck
+
+import qualified Data.MultiKeyedMap as MKM
+
+emptyMkm :: (Ord k) => MKM.MKMap k v
+emptyMkm = MKM.mkMKMap (Proxy :: Proxy Int)
+
+fromList :: [([Int], v)] -> MKM.MKMap Int v
+fromList = MKM.fromList (Proxy :: Proxy Int)
+
+prop_equality :: Property
+prop_equality =
+  forAll (resize 5 arbitrary :: Gen [([Int], Int)])
+    $ \map1 -> forAll (resize 2 arbitrary :: Gen [([Int], Int)])
+    $ \map2 ->
+        -- fromList drops empty keys, the right side would be different
+        keysNotEmpty map1 ==> keysNotEmpty map2 ==>
+        -- equality of contents also applies to the MKM
+        (map1 == map2) === (fromList map1 == fromList map2)
+        -- force the contents to be the same, should always be equal
+  where
+    keysNotEmpty = all (\(ks, _) -> ks /= [])
+
+-- | inserting the same value is idempotent
+prop_insertIdempotent :: Int -> Int -> Property
+prop_insertIdempotent key val = do
+  let insVal = MKM.insert key val
+  (insVal emptyMkm) === (insVal (insVal emptyMkm))
+
+prop_fromList_emptyKeyList :: Int -> ([Int], Int) -> Property
+prop_fromList_emptyKeyList val el = do
+  fromList [([], val), el, ([], val)] === fromList [el]
+
+-- | inserting the same values in a different order
+-- results in the same map
+prop_insertShuffled :: [(Int, Int)] -> Property
+prop_insertShuffled xs =
+  let xs' = List.nubBy ((==) `on` fst) xs
+      insVals = foldl' (\m (k, v) -> MKM.insert k v m) emptyMkm
+  in forAll (shuffle xs')
+    $ \ys -> insVals xs' === insVals ys
+
+tests :: TestTree
+tests = $(testGroupGenerator)
diff --git a/tests/TestParse.hs b/tests/TestParse.hs
new file mode 100644
--- /dev/null
+++ b/tests/TestParse.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, NamedFieldPuns, ViewPatterns, NoImplicitPrelude #-}
+module TestParse (tests) where
+
+import Protolude
+import qualified Data.Map as Map
+import Test.Tasty (TestTree)
+import Test.Tasty.TH
+import Test.Tasty.HUnit
+import NeatInterpolation
+import qualified Text.Megaparsec as MP
+import qualified Data.Char as Ch
+
+import Yarn.Lock.Types
+import Yarn.Lock.Parse
+
+-- registryPackage :: Text
+-- registryPackage = [text|
+--   accepts@1.3.3, accepts@~1.3.3:
+--     version "1.3.3"
+--     resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
+--     dependencies:
+--       mime-types "~2.1.11"
+--       negotiator "0.6.1"
+--   |]
+
+nonsenseEntry :: Text
+nonsenseEntry = [text|
+  foobar@~1.2.3, xyz@hehe:
+    field1 "°§ℓ»«UAIERNT"
+    field2 "nopedidope"
+  |]
+
+case_NonsenseASTPackageEntry :: Assertion
+case_NonsenseASTPackageEntry = do
+  parseSuccess packageEntry nonsenseEntry
+    >>= \(Keyed keys (_, PackageFields fields)) -> do
+      assertBool "two keys" (length keys == 2)
+      assertBool "two fields" (length fields == 2)
+      assertBool "field1 member" (Map.member "field1" fields)
+      assertBool "field2 member" (Map.member "field2" fields)
+      Map.lookup "field1" fields @=? (Just (Left "°§ℓ»«UAIERNT"))
+
+nestedPackage :: Text
+nestedPackage = [text|
+  readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0":
+    dependencies:
+      core-util-is "~1.0.0"
+      is.array ""
+      string_decoder "~0.10.x"
+      johnny-dep 2.3.4
+  |]
+
+case_NestedPackage :: Assertion
+case_NestedPackage = do
+  assertBool "there is unicode" (all Ch.isAscii (toS nestedPackage :: [Char]))
+  parseSuccess packageEntry nestedPackage
+    >>= \(Keyed _ (_, PackageFields fields)) -> do
+      case Map.lookup "dependencies" fields of
+        (Nothing) -> assertFailure "where’s the key"
+        (Just (Left s)) -> do
+          assertFailure $ toS (s <> "should be a nested package")
+        (Just (Right (PackageFields nested))) -> do
+          assertEqual "nested keys" 4 $ length nested
+          assertEqual "dep exists" (Just (Left "2.3.4"))
+            $ Map.lookup "johnny-dep" nested 
+
+case_PackageField :: IO ()
+case_PackageField = do
+  let goodField = "myfield12 \"abc\""
+      badField = "badbad \"abc"
+      okayishField = "f abc"
+  parseFailure field badField
+  parseSuccess field goodField
+    >>= \(key, val) -> do
+      key @=? "myfield12"
+      val @=? (Left "abc")
+  parseSuccess field okayishField
+    >>= \(key, val) -> do
+      key @=? "f"
+      val @=? (Left "abc")
+
+case_PackageKey :: Assertion
+case_PackageKey = do
+  let key = "foo@^1.3.4, bar@blafoo234, xnu@:\n"
+  parseSuccess packageKeys key
+    >>= \keys -> do
+      keys @=? [ PackageKey "foo" "^1.3.4"
+               , PackageKey "bar" "blafoo234"
+               -- yes, the version can be empty …
+               , PackageKey "xnu" ""]
+
+parseSuccess :: Parser a -> Text -> IO a
+parseSuccess parser string = do
+  case MP.parse parser "" string of
+    (Right a) -> pure a
+    (Left err) -> do
+      assertFailure ("parse should succeed, but: \n"
+                    <> MP.parseErrorPretty err
+                    <> "for input\n" <> toS string <> "\n\"")
+      panic "not reached"
+
+parseFailure :: Parser a -> Text -> IO ()
+parseFailure parser string = do
+  case MP.parseMaybe parser string of
+    Nothing -> pure ()
+    (Just _) -> assertFailure "parse should have failed"
+     
+tests :: TestTree
+tests = $(testGroupGenerator)
diff --git a/yarn-lock.cabal b/yarn-lock.cabal
--- a/yarn-lock.cabal
+++ b/yarn-lock.cabal
@@ -1,11 +1,11 @@
--- This file has been generated from package.yaml by hpack version 0.17.0.
+-- This file has been generated from package.yaml by hpack version 0.18.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           yarn-lock
-version:        0.2.0
+version:        0.3.1
 synopsis:       Represent and parse yarn.lock files
-description:    Types and parser for the lock file format of the npm successor yarn.
+description:    Types and parser for the lock file format of the npm successor yarn. All modules should be imported qualified.
 category:       Data
 homepage:       https://github.com/Profpatsch/yarn-lock#readme
 bug-reports:    https://github.com/Profpatsch/yarn-lock/issues
@@ -28,14 +28,21 @@
       src
   ghc-options: -Wall
   build-depends:
-      base == 4.*
+      base >= 4.9 && < 4.10
     , containers
     , text
-    , megaparsec == 5.*
+    , megaparsec == 5.* || == 6.*
     , protolude >= 0.1
+    , either == 4.*
   exposed-modules:
       Data.MultiKeyedMap
       Yarn.Lock
+      Yarn.Lock.File
+      Yarn.Lock.Helpers
+      Yarn.Lock.Parse
+      Yarn.Lock.Types
+  other-modules:
+      Paths_yarn_lock
   default-language: Haskell2010
 
 test-suite yarn-lock-tests
@@ -45,17 +52,22 @@
       tests
   ghc-options: -Wall
   build-depends:
-      base == 4.*
+      base >= 4.9 && < 4.10
     , containers
     , text
-    , megaparsec == 5.*
+    , megaparsec == 5.* || == 6.*
     , protolude >= 0.1
+    , either == 4.*
     , yarn-lock
     , ansi-wl-pprint >= 0.6
     , tasty >= 0.11
     , tasty-th >= 0.1.7
     , tasty-hunit >= 0.9
+    , tasty-quickcheck >= 0.8
     , protolude
+    , neat-interpolation >= 0.3
   other-modules:
-      TestLock
+      TestFile
+      TestMultiKeyedMap
+      TestParse
   default-language: Haskell2010
