diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..6638760
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,71 @@
+version: 2.1
+
+orbs:
+  codecov: codecov/codecov@1.2.2
+
+executors:
+  clojure:
+    docker:
+      - image: circleci/clojure:lein
+
+jobs:
+  clj-kondo:
+    docker:
+      - image: cljkondo/clj-kondo:2021.02.13
+    steps:
+      - checkout
+      - run: clj-kondo --lint .
+
+  test-deps-plus:
+    executor: clojure
+    steps:
+      - checkout
+      - restore_cache:
+          keys:
+            - v1-jars-{{ checksum "project.clj" }}
+            - v1-jars
+      - run: lein test-ci
+      - run:
+          command: lein eastwood
+          when: always
+      - codecov/upload
+      - store_test_results:
+          path: target/test-results
+      - save_cache:
+          key: v1-jars-{{ checksum "project.clj" }}
+          paths:
+            - ~/.m2
+
+  publish:
+    docker:
+      - image: circleci/clojure:lein
+    steps:
+      - checkout
+      - restore_cache:
+          keys:
+            - v1-jars-{{ checksum "project.clj" }}
+            - v1-jars
+      - run:
+          name: Download dependencies
+          command: lein deps
+      - run:
+          name: Publish to clojars
+          command: lein deploy
+
+workflows:
+  build-test:
+    jobs:
+      - clj-kondo
+      - test-deps-plus:
+          filters:
+            tags:
+              only: /.*/
+      - publish:
+          context: clojars-publish
+          requires:
+            - test-deps-plus
+          filters:
+            tags:
+              only: /.*/
+            branches:
+              ignore: /.*/
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..42c1f23
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,5 @@
+# Checklist
+
+- [ ] Updated the version, if [applicable](../blob/main/README.adoc#releasing)
+- [ ] Updated the [CHANGELOG.adoc](../blob/main/CHANGELOG.adoc), if applicable
+- [ ] Updated any documentation (READMEs and docstrings), if applicable
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a17fabe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+target
+classes
+checkouts
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+.lein-*
+.nrepl-port
+.eastwood
+.clj-kondo/.cache
+.idea/
+
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
new file mode 100644
index 0000000..a9653f7
--- /dev/null
+++ b/CHANGELOG.adoc
@@ -0,0 +1,16 @@
+:toc:
+:toclevels: 1
+
+= Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog].
+
+= 2021
+
+== 0.1.X - 2021-10-08
+
+=== Added
+
+* Initial release. All commits are squashed for safety reasons. The original repository lives in GitHub's CircleCI-Archived org.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..ee9bc81
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @circleci/backplane
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d921d3d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,214 @@
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from
+a Contributor if it was added to the Program by such Contributor itself or
+anyone acting on such Contributor's behalf. Contributions do not include
+additions to the Program which: (i) are separate modules of software
+distributed in conjunction with the Program under their own license
+agreement, and (ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and
+such derivative works, in source code and object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form.  This patent license shall apply to the combination of the
+Contribution and the Program if, at the time the Contribution is added by the
+Contributor, such addition of the Contribution causes such combination to be
+covered by the Licensed Patents. The patent license shall not apply to any
+other combinations which include the Contribution. No hardware per se is
+licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the licenses
+to its Contributions set forth herein, no assurances are provided by any
+Contributor that the Program does not infringe the patent or other
+intellectual property rights of any other entity. Each Contributor disclaims
+any liability to Recipient for claims brought by any other entity based on
+infringement of intellectual property rights or otherwise. As a condition to
+exercising the rights and licenses granted hereunder, each Recipient hereby
+assumes sole responsibility to secure any other intellectual property rights
+needed, if any. For example, if a third party patent license is required to
+allow Recipient to distribute the Program, it is Recipient's responsibility
+to acquire that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are offered
+by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable manner on
+or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within
+the Program.
+
+Each Contributor must identify itself as the originator of its Contribution,
+if any, in a manner that reasonably allows subsequent Recipients to identify
+the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a
+manner which does not create potential liability for other Contributors.
+Therefore, if a Contributor includes the Program in a commercial product
+offering, such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor") against any
+losses, damages and costs (collectively "Losses") arising from claims,
+lawsuits and other legal actions brought by a third party against the
+Indemnified Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the Program in
+a commercial product offering.  The obligations in this section do not apply
+to any claims or Losses relating to any actual or alleged intellectual
+property infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such claim, and
+b) allow the Commercial Contributor to control, and cooperate with the
+Commercial Contributor in, the defense and any related settlement
+negotiations. The Indemnified Contributor may participate in any such claim
+at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If
+that Commercial Contributor then makes performance claims, or offers
+warranties related to Product X, those performance claims and warranties are
+such Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a
+court requires any other Contributor to pay any damages as a result, the
+Commercial Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
+AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
+EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
+CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
+PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
+appropriateness of using and distributing the Program and assumes all risks
+associated with its exercise of rights under this Agreement , including but
+not limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment, and
+unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
+LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of the
+remainder of the terms of this Agreement, and without further action by the
+parties hereto, such provision shall be reformed to the minimum extent
+necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program itself
+(excluding combinations of the Program with other software or hardware)
+infringes such Recipient's patent(s), then such Recipient's rights granted
+under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and
+does not cure such failure in a reasonable period of time after becoming
+aware of such noncompliance. If all Recipient's rights under this Agreement
+terminate, Recipient agrees to cease use and distribution of the Program as
+soon as reasonably practicable. However, Recipient's obligations under this
+Agreement and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to
+time. No one other than the Agreement Steward has the right to modify this
+Agreement. The Eclipse Foundation is the initial Agreement Steward. The
+Eclipse Foundation may assign the responsibility to serve as the Agreement
+Steward to a suitable separate entity. Each new version of the Agreement will
+be given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the
+Agreement under which it was received. In addition, after a new version of
+the Agreement is published, Contributor may elect to distribute the Program
+(including its Contributions) under the new version. Except as expressly
+stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under this
+Agreement, whether expressly, by implication, estoppel or otherwise. All
+rights in the Program not expressly granted under this Agreement are
+reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial
+in any resulting litigation.
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..5de319d
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,264 @@
+= deps-plus plugin
+
+deps-plus is a Leiningen plugin designed to help with analyzing project dependencies and reviewing dependency changes.
+
+== Installation
+
+Add `+[com.circleci/deps-plus "0.1.0"]+` to your `+:user+` profile plugins in `+~/.lein/profiles.clj+`. For example,
+
+[source,clj]
+....
+{:user {:plugins [[com.circleci/deps-plus "0.1.0"]]}}
+....
+
+== Usage
+
+To run a deps-plus task simply run `+lein deps-plus <task name>+` from a Leiningen project.
+
+=== Tasks
+
+* *list* - Lists all dependencies of the current project.
+* *list-save* - Save the project dependency list to "deps.list".
+* *list-diff* - Compare project dependencies to those previously saved with `+list-save+`.
+* *why* - Shows all paths to given dependency as a tree. This is similar to `+lein deps :why+`, but instead of only showing the single resolved path to dependency chosen by Maven, all copies and paths to the dependency (including those excluded by conflict resolution) are shown.
+
+=== Checks
+
+* link:#check-classpath-conflicts[*check-classpath-conflicts*] - Checks for classpath conflicts.
+* link:#check-downgraded[*check-downgraded*] - Checks for dependencies which have been downgraded.
+* link:#check-families[*check-families*] - Checks for inconsistent dependency versions within families.
+* link:#check-management-conflicts[*check-management-conflicts*] - Checks for dependency management conflicts.
+* link:#check-pedantic[*check-pedantic*] - Checks for version conflicts and version ranges.
+
+==== check-classpath-conflicts
+
+Checks for classpath conflicts. Classpath conflicts occur when resources which should be unique
+(e.g. Java class files, Clojure namespaces, etc) are found in multiple dependencies. Classpath
+conflicts can lead to unpredictable behavior at runtime as the version of the resource which is loaded
+can depend on subtle factors like the ordering of JARs on the classpath, or the order in which JARs
+are merged into an uberjar. Classpath conflicts are often caused by dependencies being duplicated
+(e.g. two versions of a dependency with different Maven coordinates) or dependencies which bundle
+other dependencies (e.g. shaded JARs). In these cases the conflict can be resolved by replacing the
+offending dependency with a more appropriate one (e.g. a non-shaded version).
+
+_Example: Apache logging classes_
+
+....
+Found 6 classpath conflicts between commons-logging:commons-logging:jar:1.2:compile and org.slf4j:jcl-over-slf4j:jar:1.7.30:compile
+  org/apache/commons/logging/Log.class
+  org/apache/commons/logging/LogConfigurationException.class
+  org/apache/commons/logging/LogFactory.class
+  org/apache/commons/logging/impl/NoOpLog.class
+  org/apache/commons/logging/impl/SimpleLog$1.class
+  org/apache/commons/logging/impl/SimpleLog.class
+....
+
+These packages both implement the same classes, but the latter implements the former with SLF4J.
+
+_Solution:_ Pick which package should be chosen (there's no practical way to tell which version of
+the class has been chosen in the past).  In this case, we'll pick `org.slf4j/jcl-over-slf4j`.  So we
+add a top level exclusion for `commons-logging/commons-logging`, preventing _any_ dependency from
+pulling it in:
+
+[source,clj]
+....
+  :exclusions [... commons-logging/commons-logging ...]
+....
+
+_Example: com.google.javascript:closure-compiler_
+
+The jar for the Closure Compiler includes a shaded version of https://github.com/google/gson[Gson]
+without renaming the classes:
+
+....
+$ jar tf ~/.m2/repository/com/google/javascript/closure-compiler/v20160315/closure-compiler-v20160315.jar | grep 'com/google/gson'
+com/google/gson/
+com/google/gson/Gson$5.class
+com/google/gson/JsonNull.class
+...
+....
+
+This always causes classpath conflicts when we also declare a direct dependency on Gson, even if the
+version of Gson is the same in both places.  You'll have to deal with this type of conflict on a case
+by case basis.  Here, there is an
+https://mvnrepository.com/artifact/com.google.javascript/closure-compiler-unshaded[_unshaded_ version]
+of the Closure Compiler available that does not include the Gson classes.
+
+==== check-downgraded
+
+A dependency is considered "downgraded" if the resolved version is older than the version specified by
+some other included dependency. This can occur if `:managed-dependencies` is used to pin a dependency
+version or if Maven conflict resolution happens to not select the latest version.
+
+==== check-families
+
+Checks for inconsistent dependency versions within families. Dependencies families are sets of
+dependencies which share a common group ID and version. Inconsistent family versions can occur if the
+`:managed-dependencies` for a family are incomplete or inconsistent. This situation can also occur
+when different family members are brought in through different transitive dependency paths. To fix
+this issue consider adding a complete set of `:managed-dependencies` entries for the family.
+
+_Example: io.netty family_
+....
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-buffer:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-codec:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-common:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-transport:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-buffer:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-codec-http:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-codec:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-common:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-transport:jar:4.1.50.Final:compile
+....
+
+These netty dependencies are all in the same family, but they have different versions (`4.1.34.Final`
+vs. `4.1.50.Final`).  In this case, the `project.clj` declares a managed dependency on
+`io.netty/netty-codec-http2` but not the other members of the family:
+
+[source,clj]
+....
+  :managed-dependencies [... [io.netty/netty-codec-http2 "4.1.50.Final"] ...]
+....
+The related projects are coming in through transitive dependencies, and have different versions:
+....
+$ lein deps-plus why io.netty:netty-codec-socks:jar:4.1.34.Final
+...
+   ;; +- [circleci/my-project "0.1.0"]
+...
+   ;; |  \- [io.grpc/grpc-netty "1.20.0"]
+   ;; |     \- [io.netty/netty-handler-proxy "4.1.34.Final"]
+   ;; |        \- [io.netty/netty-codec-socks "4.1.34.Final"] (chosen)
+...
+....
+
+_Solution:_ add a managed dependency for the family.  Pick the desired version for the family (in
+this case, we'll pick `4.1.50.Final`).  At a minimum, for every package mentioned in the list of
+suspicious combinations that has the _undesired_ version, add an entry in the `:managed-dependencies`:
+
+[source,clj]
+....
+  :managed-dependencies [[io.netty/netty-codec-socks "4.1.50.Final"]
+                         [io.netty/netty-handler-proxy "4.1.50.Final"]
+                         ...]
+....
+
+For completeness, you can add _every_ member of the family the project uses to managed dependencies.
+
+==== check-management-conflicts
+
+Checks for dependency management conflicts. A dependency management conflict occurs when a dependency
+has versions specified in both `:dependencies` and `:managed-dependencies`. To resolve this issue you
+can remove the version number from `:dependencies`. If you wish to override a managed dependency
+version inherited from a parent project you should do so in your own `:managed-dependencies` section.
+
+_Example: org.clojure/core.async_
+
+....
+org.clojure:core.async:jar:1.2.603 conflicts with managed dependency org.clojure:core.async:jar:1.3.610
+....
+
+_Solution 1:_ if the exact version of `core.async` does not matter, remove the version number from
+the `org.clojure/core.async` version in your dependencies to automatically get the version provided by
+clj-parent:
+
+[source,clj]
+....
+  :dependencies [... [org.clojure/core.async] ...]
+....
+
+This solution also applies when the versions are identical:
+....
+org.clojure:core.async:jar:1.3.610 conflicts with managed dependency org.clojure:core.async:jar:1.3.610
+....
+
+_Solution 2:_ if it is necessary to pin the version `1.2.603`, move the dependency to the managed
+dependencies:
+
+[source,clj]
+....
+  :managed-dependencies [... [org.clojure/core.async "1.2.603"] ...]
+....
+
+==== check-pedantic
+
+Checks for version conflicts and version ranges. This check is similar to Leiningen’s `:pedantic?
+:abort` mode, but suggests `+:managed-dependencies+` instead of `:exclusions`.  In general, expect to
+see warnings when:
+
+1. A top-level dependency is overridden by another version
+2. A transitive dependency is overridden by an _older_ version
+
+Unlike Leiningen, this task ignores plugin dependencies since these are unaffected by managed
+dependencies. By default, each suggested managed dependency is shown alongside a dependency tree
+for the conflict. Pass the `:quiet` flag to suppress the output of these trees.
+
+_Example: cheshire_
+
+....
+Found 7 dependency conflicts.
+Considering adding the following :managed-dependencies,
+
+...
+   ;; +- [cheshire/cheshire "5.9.0"] (chosen)
+...
+   ;; \- [circleci/my-project "0.1.0"]
+   ;;    +- [circleci/the-other-project "0.1.0"]
+   ;;    |  +- [circleci/rollcage "1.0.203"]
+   ;;    |  |  \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;;    |  +- [cheshire/cheshire "5.10.0"] (omitted)
+   ;;    |  \- [amperity/vault-clj "0.7.0"]
+   ;;    |     \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;;    \- [cheshire/cheshire "5.10.0"] (omitted)
+   [cheshire/cheshire "5.9.0"]
+...
+....
+
+This shows all of the different versions of `cheshire/cheshire`, including which versions were chosen
+(would actually be used when the program runs) vs. which were excluded.  check-pedantic complains
+because multiple dependencies ask for different versions of `cheshire/cheshire`, and the newest version
+(transitively `"5.10.0"`), is omitted.
+
+_Solution 1:_ if the version of `cheshire/cheshire` from the `:dependencies` is not required for
+correctness, remove it as an explicit dependency and retry.  If the warning disappears, you can see
+that the newest version wins with `why`:
+
+....
+$ lein deps-plus why cheshire
+   ;; +- [circleci/the-other-project "0.1.0"]
+   ;; |  +- [circleci/rollcage "1.0.203"]
+   ;; |  |  \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;; |  +- [cheshire/cheshire "5.10.0"] (chosen)
+   ;; |  \- [amperity/vault-clj "0.7.0"]
+   ;; |     \- [cheshire/cheshire "5.8.1"] (omitted)
+...
+....
+
+_Solution 2:_ add a managed dependency for the preferred version.  Pick the version that should be
+included (in this case, we'll pick `"5.9.0"`).  This is the version `check-pedantic` suggests at the
+bottom of the dependency knot.  It's also the version that the project explicitly requires as a
+`:dependency`.  Move it to a managed dependency:
+
+[source,clj]
+....
+  :managed-dependencies [... [cheshire/cheshire "5.9.0"] ...]
+....
+
+Releasing
+---------
+
+New git tags are automatically published to Clojars. To find the most recent version of lein-deps-plus, look at the most recent tag in Clojars.
+
+The following should be updated on the `main` branch before tagging:
+
+- `project.clj` - version
+- `CHANGELOG.adoc` - summary of changes
+
+Using semantic versioning is recommended, increment the:
+1. MAJOR version when you make backward incompatible changes.
+2. MINOR version when you add user-facing features in a backward compatible manner.
+3. PATCH version when you make backward compatible bug fixes.
+
+License
+-------
+
+Distributed under the http://www.eclipse.org/legal/epl-v10.html[Eclipse Public License].
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..5dbecc5
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,12 @@
+coverage:
+  status:
+    project:
+      default:
+        informational: true
+    patch:
+      default:
+        informational: true
+
+comment:
+  layout: "files"
+  require_changes: true
diff --git a/project.clj b/project.clj
new file mode 100644
index 0000000..02382cc
--- /dev/null
+++ b/project.clj
@@ -0,0 +1,27 @@
+(defproject com.circleci/deps-plus "0.1.0"
+  :plugins [[jonase/eastwood "0.3.6" :exclusions [org.clojure/clojure]]]
+
+  :pedantic? :abort
+
+  :managed-dependencies [[org.clojure/clojure "1.10.1"]]
+
+  :profiles {:dev {:dependencies [[org.clojure/clojure]
+                                  [lambdaisland/kaocha "0.0-601"]
+                                  [lambdaisland/kaocha-cloverage "0.0-41"]
+                                  [lambdaisland/kaocha-junit-xml "0.0-70"]]}
+             :provided {:dependencies [[org.clojure/clojure]
+                                       [com.google.guava/guava "24.1.1-jre"]
+                                       [org.jsoup/jsoup "1.9.2"]
+                                       [leiningen/leiningen "2.9.1"]]}}
+
+  :aliases {"test"    ["run" "-m" "kaocha.runner"]
+            "test-ci" ["test"
+                       "--plugin" "cloverage"
+                       "--codecov" "--no-cov-html" "--no-cov-summary"
+                       "--plugin" "kaocha.plugin/profiling"
+                       "--plugin" "kaocha.plugin/junit-xml"
+                       "--junit-xml-file" "target/test-results/results.xml"]}
+  :repositories [["releases" {:url "https://clojars.org/repo"
+                              :username :env/clojars_username
+                              :password :env/clojars_token
+                              :sign-releases false}]])
diff --git a/src/circleci/deps_plus/core.clj b/src/circleci/deps_plus/core.clj
new file mode 100644
index 0000000..a018bf5
--- /dev/null
+++ b/src/circleci/deps_plus/core.clj
@@ -0,0 +1,514 @@
+(ns circleci.deps-plus.core
+  (:require [leiningen.core.classpath :as lein-classpath]
+            [cemerick.pomegranate.aether :as aether]
+            [circleci.deps-plus.graph :as graph]
+            [clojure.set :as set]
+            [clojure.string :as string]
+            [leiningen.core.main :as lein])
+  (:import (org.eclipse.aether DefaultRepositorySystemSession)
+           (org.eclipse.aether.graph DependencyNode)
+           (org.eclipse.aether.artifact Artifact)
+           (org.apache.maven.artifact.versioning ComparableVersion)
+           (org.eclipse.aether.collection CollectResult)
+           (java.io Writer File)
+           (java.util.jar JarFile)
+           (java.util Properties)
+           (org.eclipse.aether.util.graph.manager DependencyManagerUtils)
+           (org.eclipse.aether.resolution DependencyResult)))
+
+(def dependencies-key :dependencies)
+(def managed-dependencies-key :managed-dependencies)
+
+(defrecord Coordinates [group-id artifact-id classifier version extension]
+  Object
+  (toString [this]
+    (->> [group-id artifact-id extension classifier (str version) (:scope (meta this))]
+         (remove empty?)
+         (string/join ":"))))
+
+(defn parse-coordinates [s]
+  (let [[group-id artifact-id ext x y z] (string/split s #":")
+        ext (or ext "jar")]
+    (cond
+      (some? z)
+      (let [classifier x
+            version (ComparableVersion. y)
+            scope z]
+        (with-meta
+          (->Coordinates group-id artifact-id classifier version ext)
+          {:scope scope}))
+
+      (#{"compile" "provided" "runtime" "test"} y)
+      (let [version (ComparableVersion. x)
+            scope y]
+        (with-meta
+          (->Coordinates group-id artifact-id "" version ext)
+          {:scope scope}))
+
+      (some? y)
+      (let [classifier x
+            version (ComparableVersion. y)]
+        (->Coordinates group-id artifact-id classifier version ext))
+
+      (some? x)
+      (let [version (ComparableVersion. x)]
+        (->Coordinates group-id artifact-id "" version ext))
+
+      :else
+      (->Coordinates group-id artifact-id "" nil ext))))
+
+(defn- artifact-coordinates [^Artifact a]
+  (when a
+    (->Coordinates
+      (.getGroupId a)
+      (.getArtifactId a)
+      (.getClassifier a)
+      (some-> (.getVersion a) (ComparableVersion.))
+      (.getExtension a))))
+
+(defn- node-coordinates [^DependencyNode n]
+  (when-let [coord (artifact-coordinates (.getArtifact n))]
+    (with-meta coord
+      {:scope              (.getScope (.getDependency n))
+       :version-range      (some-> n .getVersionConstraint .getRange)
+       :premanaged-version (DependencyManagerUtils/getPremanagedVersion n)
+       :premanaged-scope   (DependencyManagerUtils/getPremanagedScope n)
+       :file               (some-> n .getArtifact .getFile)})))
+
+(defn coordinates->aether [{:keys [group-id artifact-id classifier version extension] :as dep}]
+  (let [scope (:scope (meta dep))]
+    (concat [(symbol group-id artifact-id) (str version)]
+      (when-not (string/blank? classifier) [:classifier classifier])
+      (when-not (#{"jar" nil} extension) [:extension extension])
+      (when-not (#{"compile" nil} scope) [:scope scope]))))
+
+(defn aether->coordinates [[group-artifact version & {:keys [scope classifier extension]}]]
+  (with-meta
+    (->Coordinates
+      (or (namespace group-artifact) (name group-artifact))
+      (name group-artifact)
+      (or classifier "")
+      (some-> version ComparableVersion.)
+      (or extension "jar"))
+    {:scope scope}))
+
+(defn unversioned [coordinates]
+  (assoc coordinates :version nil))
+
+(defn- basic-repository-session
+  "Returns a basic repository session that enables verbose dependency
+  management property tracking (pre-managed version, scope, etc)."
+  ^DefaultRepositorySystemSession [& args]
+  (doto ^DefaultRepositorySystemSession (apply aether/repository-session args)
+    (.setConfigProperty DependencyManagerUtils/CONFIG_PROP_VERBOSE true)))
+
+(defn- full-dependency-graph-repository-session
+  "Returns a repository session without a dependency graph transformer.
+  Without a dependency graph transformer the full dependency graph will
+  be returned by the dependency collector."
+  [& args]
+  (doto (apply basic-repository-session args)
+    (.setDependencyGraphTransformer nil)))
+
+(defn- dependency-graph [^DependencyNode root]
+  (loop [edges {} to-visit [root] visited #{root}]
+    (if (seq to-visit)
+      (let [[node & rest] to-visit
+            coordinates (node-coordinates node)
+            child-nodes (seq (.getChildren node))
+            child-coordinates (into #{} (map node-coordinates child-nodes))]
+        (recur
+          (update edges coordinates set/union child-coordinates)
+          (into rest (filter #(not (visited %)) child-nodes))
+          (into visited child-nodes)))
+      edges)))
+
+(defn- transfer-listener [e]
+  (let [{:keys [type] {:keys [name repository]} :resource} e]
+    (when (= :started type)
+      (locking *err*
+        (lein/warn "Retrieving" name "from" repository)))))
+
+(defn resolve-dependencies [project & flags]
+  (let [{:keys [repositories local-repo mirrors offline?]} project
+        flags (into #{} flags)
+        managed-coordinates (get project managed-dependencies-key)
+        coordinates (-> (get project dependencies-key)
+                        (aether/merge-versions-from-managed-coords managed-coordinates))
+        args (concat
+               [:local-repo local-repo
+                :repositories (into {} (map lein-classpath/add-repo-auth repositories))
+                :coordinates coordinates
+                :mirrors (map lein-classpath/add-repo-auth mirrors)
+                :proxy (lein-classpath/get-proxy-settings)
+                :retrieve (boolean (:retrieve flags))
+                :transfer-listener transfer-listener
+                :repository-session-fn (if (:full-graph flags)
+                                         full-dependency-graph-repository-session
+                                         basic-repository-session)
+                :offline? offline?]
+
+               (when-not (:ignore-managed-dependencies flags)
+                 [:managed-coordinates managed-coordinates]))
+        result (apply aether/resolve-dependencies* args)
+        root (if (:retrieve flags)
+               (.getRoot ^DependencyResult result)
+               (.getRoot ^CollectResult result))]
+    (dependency-graph root)))
+
+(defn- resolve-natural-dependencies
+  "Returns a graph obtained by merging the full \"natural\" dependency graphs
+  for each dependency specified by the given coordinates. Since this is a full
+  dependency graph, cyclic dependencies, version conflicts, and duplicate nodes
+  may be present. This graph can be useful for comparing the dependencies declared
+  by a given dependency to those resolved for use by the project."
+  [project coordinates]
+  (let [aether-coordinates (->> coordinates
+                                (map coordinates->aether)
+                                (into []))
+        alt-project (merge project
+                      {:dependencies         aether-coordinates
+                       :managed-dependencies nil})]
+    (resolve-dependencies alt-project :full-graph)))
+
+(defn- max*
+  "Returns the greatest value as ordered by `compare`."
+  [values]
+  (reduce
+    (fn [acc x]
+      (if (> (compare x acc) 0) x acc))
+    values))
+
+(defn check-downgraded*
+  "Returns a sequence of downgraded dependencies as described by `check-downgraded`.
+  Each downgraded dependency is returned as a pair, `[dependency max-version]`."
+  [project]
+  (let [resolved-deps (->> (resolve-dependencies project)
+                           (keys)
+                           (remove nil?)
+                           (into #{}))
+
+        ;; Get the full dependency graph obtained by depending directly on all of our
+        ;; resolved dependencies without managed dependencies in place. This allows us
+        ;; to compare the transitive dependencies that each of our dependencies would
+        ;; bring by default, to those we get with dependency management in place.
+        natural-dep-graph (resolve-natural-dependencies project resolved-deps)
+        natural-dep-versions (->> (keys natural-dep-graph)
+                                  (remove nil?)
+                                  (group-by unversioned)
+                                  (reduce-kv (fn [m k v]
+                                               (assoc m k (map :version v)))
+                                    {}))
+
+        max-versions (reduce-kv
+                       (fn [m k v] (assoc m k (max* v)))
+                       {}
+                       natural-dep-versions)]
+
+    (for [dep (sort-by str resolved-deps)
+          :let [version (:version dep)
+                max-version (get max-versions (unversioned dep))]
+          :when (< (compare version max-version) 0)]
+      [dep max-version])))
+
+(defn check-families*
+  "Returns a sequence of questionable `[child parent]` dependencies as
+  described by `check-families`."
+  [project]
+  (let [deps (->> (resolve-dependencies project)
+                  (keys)
+                  (remove nil?)
+                  (into #{}))
+
+        versions (->> deps
+                      (map (fn [dep] [(unversioned dep) dep]))
+                      (into {}))
+
+        natural-deps (resolve-natural-dependencies project deps)
+
+        same-family? (fn [a b]
+                       (let [keys [:group-id :version]]
+                         (= (select-keys a keys) (select-keys b keys))))]
+
+    (for [dep (sort-by str deps)
+          ;; Find direct dependencies of dep which, in the natural dependency graph,
+          ;; are in part of the same family as dep (same group-id and version). For
+          ;; each of these family members, lookup the actual version resolved.
+          :let [family-members (->> (get natural-deps dep)
+                                    (filter #(same-family? dep %))
+                                    (map unversioned)
+                                    (map #(get versions % (format "%s (excluded)" %))))]
+          member (sort-by str family-members)
+          :when (not= (:version member) (:version dep))]
+      [dep member])))
+
+(defn write-dependency-list
+  [project ^Writer writer]
+  (doseq [dep (->> (resolve-dependencies project)
+                   (keys)
+                   (remove nil?)
+                   (map str)
+                   (sort))]
+    (.write writer (str dep "\n")))
+  (.flush writer))
+
+(defn- parse-dependency-list [s]
+  (when (seq s)
+    (->> (string/split-lines s)
+         (map parse-coordinates))))
+
+(defn- diff-categorise
+  [^Coordinates old-dep ^Coordinates new-dep]
+  (let [old-scope (:scope (meta old-dep))
+        old-version (:version old-dep)
+        new-scope (:scope (meta new-dep))
+        new-version (:version new-dep)]
+    (cond
+      (not old-dep)
+      [:added (format "%s" new-dep)]
+
+      (not new-dep)
+      [:removed (format "%s" old-dep)]
+
+      (> (compare old-version new-version) 0)
+      [:downgraded (format "%s (%s -> %s)" new-dep old-version new-version)]
+
+      (< (compare old-version new-version) 0)
+      [:upgraded (format "%s (%s -> %s)" new-dep old-version new-version)]
+
+      (not= old-scope new-scope)
+      [:rescoped (format "%s (%s -> %s)" new-dep old-scope new-scope)])))
+
+(defn diff-dependency-list
+  [project old-list ^Writer writer]
+  (let [index #(->> (group-by unversioned %)
+                    (reduce-kv
+                     (fn [m k v]
+                       (assoc m k (first v)))
+                     {}))
+        old (->> (parse-dependency-list old-list)
+                 (index))
+        new (->> (resolve-dependencies project)
+                 (keys)
+                 (remove nil?)
+                 (index))
+        all (into #{} (concat (keys old) (keys new)))
+        groups (->> (sort-by str all)
+                    (keep #(diff-categorise (get old %) (get new %)))
+                    (group-by first))]
+    (doseq [category [:added :removed :upgraded :downgraded :rescoped]
+            :when (contains? groups category)
+            :let [changes (category groups)]]
+      (.write writer (format ";; %s\n" (string/capitalize (name category))))
+      (doseq [change changes]
+        (.write writer (format "%s\n" (second change))))
+      (.write writer "\n")))
+  (.flush writer))
+
+(defn same-dependency?
+  "Returns true if the dependencies `a` and `b` are equal ignoring version."
+  [a b]
+  (= (unversioned a) (unversioned b)))
+
+(defn- apply-managed-dependency
+  "Applies a managed dependency to a dependency graph.
+  Matching dependencies are updated to point to the managed version of
+  the dependency. The dependency graph is then pruned of no longer
+  reachable dependencies."
+  [deps managed-dep]
+  (let [managed-dep (vary-meta managed-dep dissoc :version-range)
+        replace #(if (same-dependency? managed-dep %) managed-dep %)
+        deps (reduce-kv
+               (fn [acc dep directs]
+                 (assoc acc
+                   (if (same-dependency? managed-dep dep)
+                     (vary-meta dep dissoc :version-range)
+                     dep)
+                   (into #{} (map replace directs))))
+               {}
+               deps)
+        reachable (graph/graph-seq #(get deps %) nil)]
+    (select-keys deps reachable)))
+
+(defn- pedantic-conflict-check [resolved-dep all-versions]
+  (let [v (:version resolved-dep)
+        max-v (max* (map :version all-versions))]
+    (or (< (compare v max-v) 0)
+        (some #(:version-range (meta %)) all-versions))))
+
+(defn- extract-targeted-subtree
+  "Extracts the minimal subtree from `full-graph` that captures all paths from
+  the nil root node to `target`."
+  [full-graph target]
+  (->> (graph/graph-path-seq #(get full-graph %) nil)
+       (filter #(same-dependency? (peek %) target))
+       (mapcat #(partition 2 1 %))
+       (group-by first)
+       (reduce-kv
+         (fn [acc node children]
+           (assoc acc node (into #{} (map second children))))
+         {})))
+
+(defn- find-first-conflict
+  "Find the first conflicting dependency in the given dependency graph.
+  The dependency returned is the first dependency found via a breadth
+  first search having conflicts."
+  [full-graph resolved conflict-check]
+  (let [;; For "all-versions" we look at the map values (i.e. the right-hand
+        ;; side of each graph edge) instead of the map keys, because the
+        ;; values preserve metadata, including :version-range information.
+        all-versions (->> (vals full-graph)
+                          (mapcat identity)
+                          (remove nil?)
+                          (group-by unversioned))
+        resolved-versions (reduce
+                            (fn [acc dep]
+                              (assoc acc (unversioned dep) dep))
+                            {}
+                            resolved)
+        conflict (->> (graph/graph-seq #(get full-graph %) nil)
+                      (remove nil?)
+                      (map unversioned)
+                      (filter #(conflict-check
+                                 (resolved-versions %)
+                                 (all-versions %)))
+                      (map resolved-versions)
+                      (first))]
+    (when conflict
+      [conflict (extract-targeted-subtree full-graph conflict)])))
+
+(defn find-pedantic-conflicts
+  "Finds dependency version conflicts.
+  For each conflict, returns a vector containing the chosen version of the dependency,
+  and a minimal dependency tree containing all the paths to the conflicting dependency."
+  [project]
+  (let [resolved (->> (resolve-dependencies project)
+                      (keys)
+                      (remove nil?))]
+    (loop [conflicts [] full-graph (resolve-dependencies project :full-graph)]
+      (if-let [conflict (find-first-conflict full-graph resolved pedantic-conflict-check)]
+        (recur (conj conflicts conflict)
+               (apply-managed-dependency full-graph (first conflict)))
+        conflicts))))
+
+(defn check-management-conflicts*
+  "Checks for conflicts between dependencies and managed dependencies.
+  A conflict occurs whenever a version is specified for a :dependency which
+  is also specified in :managed-dependencies. For each conflict, the
+  dependency and managed dependencies are returned as a tuple."
+  [project]
+  (let [managed-by-key (->> (:managed-dependencies project)
+                            (map aether->coordinates)
+                            (reduce
+                              (fn [acc dep]
+                                (assoc acc (unversioned dep) dep))
+                              {}))]
+    (for [dep (map aether->coordinates (:dependencies project))
+          :let [managed (managed-by-key (unversioned dep))]
+          :when (and managed (:version dep))]
+      [dep managed])))
+
+(defn resolve-targeted-dependency-subtree
+  "Returns the minimal dependency subtree that captures all paths to `target`.
+  The returned tree has a single nil root node. All leaf nodes of the returned
+  tree match `target`."
+  [project target]
+  (-> (resolve-dependencies project :full-graph)
+      (extract-targeted-subtree target)))
+
+(defn- jar-classpath-resources
+  "Returns a list of \"classpath resources\" found in the given JAR file.
+  \"Classpath resources\" are files in the JAR which should probably be unique
+  within the classpath (i.e. they should only appear in one JAR file). This
+  detection is heuristic based and may change over time."
+  [^File file]
+  (with-open [jar (JarFile. file)]
+    (->> (enumeration-seq (.entries jar))
+         (remove #(.isDirectory %))
+         (map #(.getName %))
+         ;; Limit to well known extensions. There are otherwise a lot of
+         ;; false positives which are difficult to filter out.
+         (filter #(let [ext (peek (string/split % #"\."))]
+                    (#{"class" "clj" "cljs" "cljc" "edn" "xml" "properties"} ext)))
+         ;; Maven metadata files under META-INF frequently conflict, but
+         ;; these conflicts are low risk. Some other things under META-INF
+         ;; (like Java service loader files) are merged by Leiningen.
+         (remove #(string/starts-with? % "META-INF/"))
+         ;; module-info.class -> Java 9+ module info (not an actual conflict)
+         ;; project.clj -> Leiningen project files (safe to ignore)
+         ;; data_readers.clj -> Merged by Leiningen when building an uberjar
+         (remove #{"module-info.class"
+                   "project.clj"
+                   "data_readers.clj"})
+         (doall))))
+
+(defn check-classpath-conflicts*
+  "Checks for classpath conflicts.
+  A classpath conflict occurs when multiple dependencies provide the same
+  resource. This detection applies heuristics to limit the number of false
+  positives (e.g. LICENSE files appear in multiple JARs). Conflicts are
+  returned as a map. Map keys are resource names. Map values are sets of
+  dependencies where the resource was found. Only conflicts are included so
+  these dependency sets always contain a least 2 entries."
+  [project]
+  (let [deps (resolve-dependencies project :retrieve)
+        dep-files (->> (keys deps)
+                       (remove nil?)
+                       (reduce
+                         (fn [acc dep]
+                           (if-let [file (:file (meta dep))]
+                             (assoc acc dep file)
+                             acc))
+                         {}))
+        jars (filter #(.endsWith (.getName (val %)) ".jar") dep-files)]
+    (->> (mapcat
+           (fn [[dep file]]
+             (->> (jar-classpath-resources file)
+                  (map (fn [entry] [entry dep]))))
+           jars)
+         (group-by first)
+         (reduce-kv
+           (fn [acc entry deps]
+             (if (> (count deps) 1)
+               (assoc acc entry (into [] (map second deps)))
+               acc))
+           {}))))
+
+(defn- shaded-jar-dependencies
+  "Return the coordinates for each dependency shaded within the given JAR."
+  [filename]
+  (with-open [jar (JarFile. filename)]
+    (doall (for [e (enumeration-seq (.entries jar))
+                 :when (.endsWith (.getName e) "/pom.properties")]
+             (with-open [is (.getInputStream jar e)]
+               (let [props (doto (Properties.)
+                             (.load is))
+                     group-id    (.get props "groupId")
+                     artifact-id (.get props "artifactId")
+                     version     (.get props "version")]
+                 (aether->coordinates
+                  [(symbol group-id artifact-id) version])))))))
+
+(defn who-shades*
+  "Return a map of dependencies which shade the given target dependency."
+  [project target]
+  (let [deps (resolve-dependencies project :retrieve)
+        dep-files (->> (keys deps)
+                       (remove nil?)
+                       (remove #(same-dependency? target %))
+                       (reduce
+                         (fn [acc dep]
+                           (let [file (:file (meta dep))]
+                             (if (some-> file .getName (.endsWith ".jar"))
+                               (assoc acc dep file)
+                               acc)))
+                         {}))]
+    (reduce-kv
+     (fn [acc dep jar-path]
+       (if-let [version (->> (shaded-jar-dependencies jar-path)
+                             (filter #(same-dependency? target %))
+                             (first))]
+         (assoc acc dep version)
+         acc))
+     {}
+     dep-files)))
diff --git a/src/circleci/deps_plus/graph.clj b/src/circleci/deps_plus/graph.clj
new file mode 100644
index 0000000..834d6fe
--- /dev/null
+++ b/src/circleci/deps_plus/graph.clj
@@ -0,0 +1,29 @@
+(ns circleci.deps-plus.graph)
+
+(defn graph-path-seq
+  "Returns a sequence of non-cyclic paths found via a breadth-first walk.
+  Equivalent to `graph-seq`, but each node is returned as a vector of nodes
+  represent the path from `root` (the first element) to the visited node (the last element).
+  `neighbors` must be a fn of one arg that returns the nodes adjacent to the given node.
+  `root` is the starting node."
+  [neighbors root]
+  (loop [to-visit [[root]] visited []]
+    (if (seq to-visit)
+      (let [path (first to-visit)
+            path-set (into #{} path)
+            adj (->> (neighbors (peek path))
+                     (remove path-set))
+            adj-paths (map #(conj path %) adj)]
+        (recur
+          (into (subvec to-visit 1) adj-paths)
+          (conj visited path)))
+      (seq visited))))
+
+(defn graph-seq
+  "Returns a sequence of graph nodes found via a breadth-first walk. Inspired by `tree-seq`.
+  `neighbors` must be a fn of one arg that returns the nodes adjacent to the given node.
+  `root` is the starting node."
+  [neighbors root]
+  (->> (graph-path-seq neighbors root)
+       (map peek)
+       (distinct)))
diff --git a/src/leiningen/deps_plus.clj b/src/leiningen/deps_plus.clj
new file mode 100644
index 0000000..6d50298
--- /dev/null
+++ b/src/leiningen/deps_plus.clj
@@ -0,0 +1,275 @@
+(ns leiningen.deps-plus
+  (:refer-clojure :exclude [list])
+  (:require [leiningen.core.main :as lein]
+            [circleci.deps-plus.core :as deps]
+            [clojure.java.io :as io]
+            [leiningen.core.project :as project]
+            [clojure.string :as str]))
+
+(defn- list
+  "Lists all dependencies of the current project."
+  [project]
+  (deps/write-dependency-list project *out*))
+
+(defn- list-save
+  "Save a list of all dependencies of the current project."
+  [project]
+  (with-open [out (io/writer "deps.list")]
+    (deps/write-dependency-list project out)))
+
+(defn- list-diff
+  "Compare current dependencies to those previously saved with `list-save`."
+  [project]
+  (let [in (io/file "deps.list")]
+    (when-not (.exists in)
+      (lein/warn "No deps.list file found. list-save must be run before list-diff.")
+      (lein/exit 1))
+    (deps/diff-dependency-list project (slurp in) *out*)))
+
+(defn- check-downgraded
+  "Checks for dependencies which have been downgraded.
+  A dependency is considered \"downgraded\" if the resolved version is older
+  than the version specified by some other included dependency. This can occur
+  if `:managed-dependencies` is used to pin a dependency version or if Maven
+  conflict resolution happens to not select the latest version."
+  [project]
+  (doseq [[dep max-version] (deps/check-downgraded* project)]
+    (lein/info (format "%s downgraded to %s"
+                 (assoc dep :version max-version)
+                 (:version dep)))))
+
+(defn- check-families
+  "Checks for inconsistent dependency versions within families.
+  Dependencies families are sets of dependencies which share a common group ID
+  and version. Inconsistent family versions can occur if the `:managed-dependencies`
+  for a family are incomplete or inconsistent. This situation can also occur
+  when different family members are brought in through different transitive
+  dependency paths. To fix this issue consider adding a complete set of
+  `:managed-dependencies` entries for the family."
+  [project]
+  (when-let [combos (seq (deps/check-families* project))]
+    (lein/info (format "Found %d suspicious combinations." (count combos)))
+    (doseq [[parent child] combos]
+      (lein/info "Suspicious combination," (str parent) "and" (str child)))))
+
+(defn- format-dependency-tree-node
+  "Formats the dependency in Leiningen style."
+  [dep]
+  (let [dep (deps/coordinates->aether dep)]
+    ;; A Leiningen dependency is formatted as,
+    ;;
+    ;;   [group/artifact "version" :param-key "param-value" ...]
+    ;;
+    ;; We rely on the fact that unquoted and quoted values alternate
+    ;; to convert to a string representation.
+    (str "[" (->> (partition 2 dep)
+                  (map #(apply format "%s \"%s\"" %))
+                  (str/join " ")) "]")))
+
+(defn- print-dependency-tree
+  "Prints a human readable dependency tree.
+  `deps` should be a dependency tree encoded as a map from coordinates to
+  collections of direct dependency coordinates. If provided, `resolved` should
+  be a set of resolved dependency coordinates. Matching dependencies will then
+  be annotated in the dependency graph output."
+  [deps resolved]
+  (let [;; Build a sequence of tree paths in depth first order (i.e. the order
+        ;; we need to print them in). Each path element is a `[node is-last?]`
+        ;; tuple where "is-last?" indicates if the node is the last child of
+        ;; its parent node.
+        paths (tree-seq
+                (constantly true)
+                (fn [path]
+                  (let [[node] (peek path)
+                        children (get deps node)
+                        last-idx (dec (count children))]
+                    (->> (map (fn [child idx] [child (= idx last-idx)])
+                              children (range))
+                         (map #(conj path %)))))
+                [[nil true]])
+        ;; Every path begins with the nil root node.
+        ;; Remove it to simplify output.
+        paths (->> (map #(subvec % 1) paths)
+                   (remove empty?))]
+    (doseq [path paths]
+      (let [[node last?] (peek path)
+            prefix-parts (concat
+                           (->> (pop path)
+                                (map second)
+                                (map (fn [last?] (if last? "   " "|  "))))
+                           [(if last? "\\- " "+- ")])
+            prefix (apply str prefix-parts)
+            annotated? (some #(deps/same-dependency? % node) resolved)
+            version-range (:version-range (meta node))
+            premanaged-version (or (:premanaged-version (meta node))
+                                 (:version node))
+            suffix (cond
+                     ;; Dependency should not be annotated.
+                     (not annotated?)
+                     ""
+
+                     ;; Version was omitted by conflict resolution.
+                     (not (resolved node))
+                     " (omitted)"
+
+                     ;; Version was resolved from version range.
+                     version-range
+                     (str " (chosen from range " version-range ")")
+
+                     ;; Version was chosen by :managed-dependencies.
+                     (not= (str premanaged-version) (str (:version node)))
+                     (str " (managed from " premanaged-version ")")
+
+                     ;; Version matches the version chosen.
+                     :else " (chosen)")]
+        (lein/info (str "   ;; " prefix (format-dependency-tree-node node) suffix))))))
+
+(defn- check-pedantic
+  "Checks for dependency conflicts and shows suggested :managed-dependencies.
+  Version conflicts occur when dependency resolution selects a version of a
+  dependency that is older than one appearing elsewhere in the dependency
+  graph (e.g. clj-time 0.14.0 is chosen at the exclusion of 0.15.0). Conflicts
+  are also reported any time a version range is encountered in the dependency
+  graph.
+
+  The output from this task is a set of suggested managed dependencies. For
+  each managed dependency a dependency tree is also output that shows how the
+  conflicting dependency is depended upon. To suppress the outputting of these
+  trees pass the \":quiet\" flag.
+
+  This check is similar to Leiningen's `:pedantic? :abort` mode, but shows
+  suggested :managed-dependencies instead of :exclusions. Unlike Leiningen,
+  plugin dependencies are ignored by this task."
+  [project & args]
+  (when-let [conflicts (seq (sort-by str (deps/find-pedantic-conflicts project)))]
+    (lein/info (format "Found %d dependency conflicts." (count conflicts)))
+    (lein/info "Considering adding the following :managed-dependencies,")
+    (doseq [[dep conflict-tree] conflicts]
+      (when-not (some #{":quiet"} args)
+        (lein/info "")
+        (print-dependency-tree conflict-tree #{dep}))
+      (lein/info (format "   [%s/%s \"%s\"]"
+                   (:group-id dep) (:artifact-id dep) (:version dep))))))
+
+(defn- check-management-conflicts
+  "Checks for dependency management conflicts.
+  A dependency management conflict occurs when a dependency has versions
+  specified in both :dependencies and :managed-dependencies."
+  [project]
+  (when-let [conflicts (seq (deps/check-management-conflicts* project))]
+    (lein/info (format "Found %d management conflicts." (count conflicts)))
+    (doseq [[dep managed] conflicts]
+      (lein/info (format "%s conflicts with managed dependency %s"
+                         dep managed)))))
+
+(defn- check-classpath-conflicts
+  "Checks for classpath conflicts.
+  Classpath conflicts occur when resources which should be unique (e.g. Java
+  class files, Clojure namespaces, etc) are found in multiple dependencies.
+  Classpath conflicts can lead to unpredictable behavior at runtime as the
+  version of the resource which is loaded can depend on subtle factors like
+  the ordering of JARs on the classpath, or the order in which JARs are merged
+  into an uberjar. Classpath conflicts are often caused by dependencies being
+  duplicated (e.g. two versions of a dependency with different Maven
+  coordinates) or dependencies which bundle other dependencies (e.g. shaded
+  JARs). In these cases the conflict can be resolved by replacing the offending
+  dependency with a more appropriate one (e.g. a non-shaded version)."
+  [project]
+  (let [conflicts (deps/check-classpath-conflicts* project)
+        ;; Index conflicts by the sets of dependencies they appear in
+        conflicts-by-set (->> (group-by val conflicts)
+                              (reduce-kv
+                                (fn [acc k entries]
+                                  (assoc acc k (into [] (map key entries))))
+                                {}))
+        format-dep-set (fn [dep-set]
+                         (let [deps (into [] (sort (map str dep-set)))]
+                           (str (str/join ", " (pop deps)) " and " (peek deps))))
+        ;; Pre-format the dependency set keys as strings so we can
+        ;; sort the output and provide a stable output order.
+        conflicts-by-set-str (reduce-kv
+                               (fn [acc dep-set conflicts]
+                                 (assoc acc
+                                   (format-dep-set dep-set)
+                                   conflicts))
+                               {}
+                               conflicts-by-set)]
+    (when-not (empty? conflicts)
+      (lein/info (format "Found %d classpath conflicts between %d dependencies."
+                   (count conflicts)
+                   (->> conflicts-by-set keys (reduce into) count)))
+      (doseq [[dep-set-str conflicts] (sort conflicts-by-set-str)]
+        (lein/info "")
+        (lein/info (format "Found %d classpath conflicts between %s"
+                     (count conflicts) dep-set-str))
+        (let [n 10]
+          (doseq [entry (take n (sort conflicts))]
+            (lein/info " " entry))
+          (when (< n (count conflicts))
+            (lein/info " " (- (count conflicts) n) "more...")))))))
+
+(defn- parse-target-dependency [^String target]
+  (if (.contains target ":")
+    (deps/parse-coordinates target)
+    (deps/aether->coordinates [(symbol target) nil])))
+
+(defn- why
+  "Show all paths to the given dependency as a tree.
+  This is similar to `lein deps :why`, but all copies and paths to the given
+  dependency are shown."
+  [project group-name]
+  (let [target (parse-target-dependency group-name)
+        target (->> (deps/resolve-dependencies project)
+                    (keys)
+                    (filter #(deps/same-dependency? % target))
+                    (first))]
+    (if target
+      (let [subtree (deps/resolve-targeted-dependency-subtree project target)]
+        (print-dependency-tree subtree #{target}))
+      (lein/warn "No dependency found matching" group-name))))
+
+(defn- who-shades
+  "Show project dependencies which shade the given dependency."
+  [project group-name]
+  (let [target (parse-target-dependency group-name)]
+    (doseq [[dep target-version] (deps/who-shades* project target)]
+      (println (str dep) "shades" (str target-version)))))
+
+(defn- error [project message]
+  (lein/warn "[ERROR]" message "\n")
+  (lein/resolve-and-apply project ["help" "deps-plus"])
+  (lein/exit 1 message))
+
+(defn- with-managed-deps
+  "Run a Leiningen task with the projects managed dependencies used as its dependencies."
+  [project args]
+  (let [dependencies (->> (:managed-dependencies project)
+                          (map (fn [[name _ & rest]]
+                                 (into [name nil] rest))))
+        project (project/merge-profiles project [{:dependencies dependencies}])]
+    (lein/resolve-and-apply project args)))
+
+(defn deps-plus
+  "Extra tasks for analyzing dependencies"
+  {:subtasks [#'list #'list-save #'list-diff #'check-downgraded #'check-families #'check-pedantic
+              #'check-management-conflicts #'check-classpath-conflicts #'why #'who-shades]}
+  [project & [sub-task & args]]
+  (let [project (project/unmerge-profiles project [:base :user])]
+    (case sub-task
+      "list" (list project)
+      "list-save" (list-save project)
+      "list-diff" (list-diff project)
+      "check-downgraded" (check-downgraded project)
+      "check-families" (check-families project)
+      "check-pedantic" (apply check-pedantic project args)
+      "check-management-conflicts" (check-management-conflicts project)
+      "check-classpath-conflicts" (check-classpath-conflicts project)
+      "why" (apply why project args)
+      "who-shades" (apply who-shades project args)
+      "with-managed-deps" (with-managed-deps project args)
+      ;; Leiningen is supposed to translate "lein <task> help" into
+      ;; "lein help <task>" by default. For some reason it does not
+      ;; seem to be working.
+      "help" (lein/resolve-and-apply project (concat ["help" "deps-plus"] args))
+      nil (error project "Task required.")
+      (error project "Unknown task."))))
diff --git a/test-projects/classpath-conflict.clj b/test-projects/classpath-conflict.clj
new file mode 100644
index 0000000..2466977
--- /dev/null
+++ b/test-projects/classpath-conflict.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/classpath-conflict "0-SNAPSHOT"
+  :dependencies [[com.google.guava/guava "17.0"]
+                 [com.google.guava/guava-jdk5 "17.0"]])
diff --git a/test-projects/dependency-diff.clj b/test-projects/dependency-diff.clj
new file mode 100644
index 0000000..67c95c4
--- /dev/null
+++ b/test-projects/dependency-diff.clj
@@ -0,0 +1,10 @@
+(defproject deps-plus-tests/dependency-diff "0-SNAPSHOT"
+  :offline? true
+  :dependencies [[fixture/added "1.0"]
+                 [fixture/changed-classifier "1.0"]
+                 [fixture/changed-extension "1.0"]
+                 [fixture/changed-scope "1.0"]
+                 [fixture/downgraded "0.9"]
+                 ;; [fixture/removed "1.0"]
+                 [fixture/unmodified "1.0"]
+                 [fixture/upgraded "2.0"]])
diff --git a/test-projects/dependency-diff.list b/test-projects/dependency-diff.list
new file mode 100644
index 0000000..d2fae44
--- /dev/null
+++ b/test-projects/dependency-diff.list
@@ -0,0 +1,7 @@
+fixture:changed-classifier:jar:sources:1.0:compile
+fixture:changed-extension:tar.gz:1.0:compile
+fixture:changed-scope:jar:1.0:test
+fixture:downgraded:jar:1.0:compile
+fixture:removed:jar:1.0:compile
+fixture:unmodified:jar:1.0:compile
+fixture:upgraded:jar:1.0:compile
diff --git a/test-projects/dependency-list.clj b/test-projects/dependency-list.clj
new file mode 100644
index 0000000..4b51799
--- /dev/null
+++ b/test-projects/dependency-list.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/dependency-list "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-context "1.20.0"]
+                 [com.google.code.gson/gson "2.7"]])
diff --git a/test-projects/downgrade.clj b/test-projects/downgrade.clj
new file mode 100644
index 0000000..8182aa7
--- /dev/null
+++ b/test-projects/downgrade.clj
@@ -0,0 +1,4 @@
+(defproject deps-plus-tests/family-mismatch "0-SNAPSHOT"
+  ;; grpc-core 1.20 depends on gson 2.7
+  :managed-dependencies [[com.google.code.gson/gson "2.6"]]
+  :dependencies [[io.grpc/grpc-core "1.20.0"]])
diff --git a/test-projects/family-mismatch.clj b/test-projects/family-mismatch.clj
new file mode 100644
index 0000000..08199b9
--- /dev/null
+++ b/test-projects/family-mismatch.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/family-mismatch "0-SNAPSHOT"
+  :managed-dependencies [[io.grpc/grpc-context "1.19.0"]]
+  :dependencies [[io.grpc/grpc-core "1.20.0"]])
diff --git a/test-projects/management-conflict.clj b/test-projects/management-conflict.clj
new file mode 100644
index 0000000..17e14c5
--- /dev/null
+++ b/test-projects/management-conflict.clj
@@ -0,0 +1,5 @@
+(defproject deps-plus-tests/management-conflict "0-SNAPSHOT"
+  :managed-dependencies [[io.grpc/grpc-api "1.21.0"]
+                         [io.grpc/grpc-core "1.21.0"]]
+  :dependencies [[io.grpc/grpc-api "1.22.0"]
+                 [io.grpc/grpc-core]])
diff --git a/test-projects/pedantic-range.clj b/test-projects/pedantic-range.clj
new file mode 100644
index 0000000..42d6608
--- /dev/null
+++ b/test-projects/pedantic-range.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/pedantic-range "0-SNAPSHOT"
+  ;; grpc-grpclb has a version ranged dependency on grpc-core
+  :dependencies [[io.grpc/grpc-grpclb "1.19.0"]])
diff --git a/test-projects/pedantic-version-conflict.clj b/test-projects/pedantic-version-conflict.clj
new file mode 100644
index 0000000..7d0f4d1
--- /dev/null
+++ b/test-projects/pedantic-version-conflict.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/pedantic-version-conflict "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-protobuf "1.20.0"]
+                 [io.grpc/grpc-grpclb "1.19.0"]])
diff --git a/test-projects/shaded-dependencies.clj b/test-projects/shaded-dependencies.clj
new file mode 100644
index 0000000..63da9e2
--- /dev/null
+++ b/test-projects/shaded-dependencies.clj
@@ -0,0 +1,7 @@
+(defproject deps-plus-tests/classpath-conflict "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-netty-shaded "1.20.0"]
+                 ;; Included to ensure we also have unshaded copies of Netty
+                 ;; dependencies to search. We do not want who-shades to return
+                 ;; self matches (e.g. netty-common does not shade netty-common).
+                 [io.grpc/grpc-netty "1.20.0"]
+                 [io.honeycomb.libhoney/libhoney-java "1.3.1"]])
diff --git a/test/circleci/deps_plus/core_test.clj b/test/circleci/deps_plus/core_test.clj
new file mode 100644
index 0000000..65f3af3
--- /dev/null
+++ b/test/circleci/deps_plus/core_test.clj
@@ -0,0 +1,274 @@
+(ns circleci.deps-plus.core-test
+  (:require [clojure.test :refer (deftest testing is use-fixtures)])
+  (:require [circleci.deps-plus.core :as deps]
+            [leiningen.core.project :as lein-project]
+            [clojure.string :as str])
+  (:import (org.apache.maven.artifact.versioning ComparableVersion)
+           (java.io StringWriter)))
+
+;; Needed when not running with :eval-in :leiningen (which would do this for us)
+(use-fixtures :once
+  (fn [f]
+    (lein-project/ensure-dynamic-classloader)
+    (f)))
+
+(deftest check-families-tests
+  (testing "check-families*"
+    (let [project (lein-project/read "test-projects/family-mismatch.clj")
+          problems (into [] (deps/check-families* project))
+          [parent child] (first problems)]
+      (is (= 1 (count problems)))
+      (is (= "grpc-core" (:artifact-id parent)))
+      (is (= "grpc-context" (:artifact-id child)))
+      (is (= "1.20.0" (str (:version parent))))
+      (is (= "1.19.0" (str (:version child)))))))
+
+(deftest check-downgraded-tests
+  (testing "check-downgraded*"
+    (let [project (lein-project/read "test-projects/downgrade.clj")
+          problems (into [] (deps/check-downgraded* project))
+          [dep max-version] (first problems)]
+      (is (= 1 (count problems)))
+      (is (= "com.google.code.gson:gson:jar:2.6:compile" (str dep)))
+      (is (= "2.7" (str max-version))))))
+
+(deftest parse-coordinates
+  (testing "minimal coordinates"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (nil? (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "minimal coordinates with extension"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:tar.gz")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "tar.gz" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (nil? (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "basic coordinates"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:1.20.0")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "with scope"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:1.20.0:provided")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (= "provided" (:scope (meta coord))))))
+
+  (testing "with classifier"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:sources:1.20.0")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "sources" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "with scope and classifier"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:sources:1.20.0:provided")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "sources" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (= "provided" (:scope (meta coord)))))))
+
+(deftest write-dependency-list
+  (let [project (lein-project/read "test-projects/dependency-list.clj" [])
+        buffer (StringWriter.)
+        _ (deps/write-dependency-list project buffer)
+        lines (str/split-lines (str buffer))]
+    (is (= "com.google.code.gson:gson:jar:2.7:compile" (first lines)))
+    (is (= "io.grpc:grpc-context:jar:1.20.0:compile" (second lines)))))
+
+(deftest diff-dependency-list
+  (let [project (lein-project/read "test-projects/dependency-diff.clj" [])
+        list (slurp "test-projects/dependency-diff.list")
+        buffer (StringWriter.)
+        _ (deps/diff-dependency-list project list buffer)
+        lines (str/split-lines (str buffer))]
+    (is (= [";; Added"
+            "fixture:added:jar:1.0:compile"
+            "fixture:changed-classifier:jar:1.0:compile"
+            "fixture:changed-extension:jar:1.0:compile"
+            ""
+            ";; Removed"
+            "fixture:changed-classifier:jar:sources:1.0:compile"
+            "fixture:changed-extension:tar.gz:1.0:compile"
+            "fixture:removed:jar:1.0:compile"
+            ""
+            ";; Upgraded"
+            "fixture:upgraded:jar:2.0:compile (1.0 -> 2.0)"
+            ""
+            ";; Downgraded"
+            "fixture:downgraded:jar:0.9:compile (1.0 -> 0.9)"
+            ""
+            ";; Rescoped"
+            "fixture:changed-scope:jar:1.0:compile (test -> compile)"]
+           lines)))
+  (testing "empty sections aren't included"
+    (let [project (lein-project/read "test-projects/dependency-diff.clj" [])
+          buffer (StringWriter.)
+          _ (deps/diff-dependency-list project "" buffer)
+          out (str buffer)]
+      (is (= [";; Added"]
+             (filter (fn [line] (str/starts-with? line ";; "))
+                     (str/split-lines out)))))))
+
+(deftest find-pedantic-conflicts-tests
+  (testing "version conflict"
+    (let [project (lein-project/read "test-projects/pedantic-version-conflict.clj" [])
+          conflicts (deps/find-pedantic-conflicts project)]
+      (is (= 1 (count conflicts)))
+      (is (= "io.grpc:grpc-core:jar:1.19.0:compile" (str (first (first conflicts)))))))
+
+  (testing "version range"
+    (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+          conflicts (deps/find-pedantic-conflicts project)]
+      (is (= 1 (count conflicts)))
+      (is (= "io.grpc:grpc-core:jar:1.19.0:compile" (str (first (first conflicts))))))))
+
+(deftest parse-dependency-list-test
+  (testing "empty list"
+    (let [deps (#'deps/parse-dependency-list "")]
+      (is (empty? deps))))
+
+  (testing "one dependency"
+    (let [deps (#'deps/parse-dependency-list "io.grpc:grpc-core:jar:1.19.0:compile")
+          {:keys [group-id artifact-id extension classifier version]} (first deps)]
+      (is (= 1 (count deps)))
+      (is (= "io.grpc" group-id))
+      (is (= "grpc-core" artifact-id))
+      (is (= "jar" extension))
+      (is (= "" classifier))
+      (is (= (ComparableVersion. "1.19.0") version))
+      (is (= "compile" (:scope (meta (first deps)))))))
+
+  (testing "multiple dependencies"
+    (let [deps (#'deps/parse-dependency-list (str "io.grpc:grpc-core:jar:1.19.0:compile\n"
+                                             "io.grpc:grpc-api:jar:1.21.0:compile\n"))]
+      (is (= 2 (count deps)))
+      (let [{:keys [group-id artifact-id extension classifier version]} (first deps)]
+        (is (= "io.grpc" group-id))
+        (is (= "grpc-core" artifact-id))
+        (is (= "jar" extension))
+        (is (= "" classifier))
+        (is (= (ComparableVersion. "1.19.0") version))
+        (is (= "compile" (:scope (meta (first deps))))))
+      (let [{:keys [group-id artifact-id extension classifier version]} (second deps)]
+        (is (= "io.grpc" group-id))
+        (is (= "grpc-api" artifact-id))
+        (is (= "jar" extension))
+        (is (= "" classifier))
+        (is (= (ComparableVersion. "1.21.0") version))
+        (is (= "compile" (:scope (meta (first deps)))))))))
+
+(deftest check-management-conflicts-tests
+  (testing "management conflict"
+    (let [project (lein-project/read "test-projects/management-conflict.clj" [])
+          conflicts (deps/check-management-conflicts* project)]
+      (is (= 1 (count conflicts)))
+      (let [[dep managed] (first conflicts)]
+        (is (= "io.grpc:grpc-api:jar:1.22.0" (str dep)))
+        (is (= "io.grpc:grpc-api:jar:1.21.0" (str managed)))))))
+
+(deftest resolve-dependencies
+  (testing "standard resolution includes ranges"
+    (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+          resolved (deps/resolve-dependencies project)
+          grpc-cores (->> (vals resolved)
+                          (mapcat identity)
+                          (filter #(= "grpc-core" (:artifact-id %))))]
+      (is (< 0 (count grpc-cores)))
+      ;; Every copy should be identical and should therefore have a version range.
+      (is (every? (comp :version-range meta) grpc-cores))
+      (is (= "[1.19.0,1.19.0]" (str (:version-range (meta (first grpc-cores))))))))
+
+  (testing "full-graph resolution includes ranges"
+      (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+            resolved (deps/resolve-dependencies project :full-graph)
+            grpc-cores (->> (vals resolved)
+                            (mapcat identity)
+                            (filter #(= "grpc-core" (:artifact-id %))))]
+        ;; We should have copies both with and without version ranges
+        (is (< 0 (count (filter (comp :version-range meta) grpc-cores))))
+        (is (< 0 (count (remove (comp :version-range meta) grpc-cores))))))
+
+  (testing "standard resolution includes premanaged version"
+    (let [project (lein-project/read "test-projects/downgrade.clj" [])
+          resolved (deps/resolve-dependencies project)
+          gson (->> (keys resolved)
+                    (filter #(= "gson" (:artifact-id %)))
+                    (first))]
+      (is (some? gson))
+      (is (= "2.6" (str (:version gson))))
+      (is (= "2.7" (str (:premanaged-version (meta gson)))))))
+
+  (testing "full-graph resolution includes premanaged version"
+    (let [project (lein-project/read "test-projects/downgrade.clj" [])
+          resolved (deps/resolve-dependencies project :full-graph)
+          gson (->> (keys resolved)
+                    (filter #(= "gson" (:artifact-id %)))
+                    (first))]
+      (is (some? gson))
+      (is (= "2.6" (str (:version gson))))
+      (is (= "2.7" (str (:premanaged-version (meta gson))))))))
+
+(deftest check-classpath-conflicts-test
+  (let [project (lein-project/read "test-projects/classpath-conflict.clj" [])
+        conflicts (deps/check-classpath-conflicts* project)
+        conflicting-deps (get conflicts "com/google/common/base/Preconditions.class")]
+    (is (= 2 (count conflicting-deps)))
+    (is (= #{"guava" "guava-jdk5"} (into #{} (map :artifact-id conflicting-deps))))))
+
+(deftest who-shades
+  (testing "dependency only appearing as shaded"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "commons-codec:commons-codec")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.honeycomb.libhoney:libhoney-java:jar:1.3.1:compile" (str shader)))
+        (is (= "commons-codec:commons-codec:jar:1.9" (str shadee))))))
+
+  ;; This case validates that who-shades does not return self
+  ;; matches (e.g. netty-common does not shade itself).
+  (testing "dependency appearing as both shaded and standalone"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "io.netty:netty-common")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.grpc:grpc-netty-shaded:jar:1.20.0:compile" (str shader)))
+        (is (= "io.netty:netty-common:jar:4.1.34.Final" (str shadee))))))
+
+  ;; This behavior could be changed, but at least at the moment the version
+  ;; number in the target is ignored.
+  (testing "dependency only appearing as shaded"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "commons-codec:commons-codec:jar:1.2.3")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.honeycomb.libhoney:libhoney-java:jar:1.3.1:compile" (str shader)))
+        (is (= "commons-codec:commons-codec:jar:1.9" (str shadee))))))
+
+  (testing "no matches"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "super-fake:so-fake:jar:what-big-versions")
+          result  (deps/who-shades* project target)]
+      (is (empty? result)))))
diff --git a/test/circleci/deps_plus/graph_test.clj b/test/circleci/deps_plus/graph_test.clj
new file mode 100644
index 0000000..0aa78b5
--- /dev/null
+++ b/test/circleci/deps_plus/graph_test.clj
@@ -0,0 +1,58 @@
+(ns circleci.deps-plus.graph-test
+  (:require [clojure.test :refer [deftest is testing]])
+  (:require [circleci.deps-plus.graph :as graph]))
+
+(defn- neighbors-fn [g]
+  (fn [n] (sort (get g n))))
+
+(deftest graph-seq
+  (testing "break cycle"
+    (let [g {:a #{:b}
+             :b #{:a}}]
+      (is (= [:a :b]
+            (into [] (graph/graph-seq (neighbors-fn g) :a))))))
+
+  (testing "breadth first"
+    (let [g {:a #{:b :c}
+             :b #{:d :e}
+             :c #{:f :g}}]
+      (is (= [:a :b :c :d :e :f :g]
+            (into [] (graph/graph-seq (neighbors-fn g) :a))))))
+
+  (testing "remove duplicates"
+    (let [g {:a #{:b :c}
+             :b #{:c}}]
+      (is (= [:a :b :c]
+            (into [] (graph/graph-seq (neighbors-fn g) :a)))))))
+
+(deftest graph-path-seq
+  (testing "break cycle"
+    (let [g {:a #{:b}
+             :b #{:c}
+             :c #{:b}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :b :c]]
+             (into [] (graph/graph-path-seq (neighbors-fn g) :a))))))
+
+  (testing "breadth first"
+    (let [g {:a #{:b :c}
+             :b #{:d :e}
+             :c #{:f :g}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :c]
+              [:a :b :d]
+              [:a :b :e]
+              [:a :c :f]
+              [:a :c :g]]
+             (into [] (graph/graph-path-seq (neighbors-fn g) :a))))))
+
+  (testing "follow all paths"
+    (let [g {:a #{:b :c}
+             :b #{:c}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :c]
+              [:a :b :c]]
+            (into [] (graph/graph-path-seq (neighbors-fn g) :a)))))))