Repository: mozilla/cargo-vet
Pull request #214 (2022-06-22 10:41:21)

Bump clap from 3.2.5 to 3.2.6
Bumps [clap](https://github.com/clap-rs/clap) from 3.2.5 to 3.2.6.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's changelog</a>.</em></p>
<blockquote>
<h2>[3.2.6] - 2022-06-21</h2>
<h3>Fixes</h3>
<ul>
<li>Don't panic when parsing <code>--=</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/clap-rs/clap/commit/3823df9bbe03efefcb49319834a72cf2cfa2ede9"><code>3823df9</code></a> chore: Release</li>
<li><a href="https://github.com/clap-rs/clap/commit/8a659bc2b43c33d57597da3c14156f9e2ef4f235"><code>8a659bc</code></a> docs: Update changelog</li>
<li><a href="https://github.com/clap-rs/clap/commit/ceb7ee4f54fba8681f418a4d1e92ed3d91f30cbe"><code>ceb7ee4</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/clap-rs/clap/issues/3860">#3860</a> from epage/panic</li>
<li><a href="https://github.com/clap-rs/clap/commit/01da890145adab9b1993ffde57af3788c7ed00a3"><code>01da890</code></a> fix(parser): Don't panic on <code>--=</code></li>
<li><a href="https://github.com/clap-rs/clap/commit/dabb5713d4d60b52deaef21c42c9d9dae57f5def"><code>dabb571</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/clap-rs/clap/issues/3853">#3853</a> from dtolnay-contrib/valuevalidation</li>
<li><a href="https://github.com/clap-rs/clap/commit/690530a19120e47415026498370ee57d74aad721"><code>690530a</code></a> Fix typo in ErrorKind::ValueValidation message</li>
<li><a href="https://github.com/clap-rs/clap/commit/ae78f88d48cfd64cd93875d54103a51cdf7e5382"><code>ae78f88</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/clap-rs/clap/issues/3852">#3852</a> from epage/msrv</li>
<li><a href="https://github.com/clap-rs/clap/commit/82d70d82c46925339229048bbea9d2a6a8c3843a"><code>82d70d8</code></a> chore: Bump MSRV from 1.56.0 to 1.56.1</li>
<li><a href="https://github.com/clap-rs/clap/commit/8c28bec3960aebae7eb9ec1509aa8d5ad2760c7f"><code>8c28bec</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/clap-rs/clap/issues/3851">#3851</a> from tshepang/trim-manifest</li>
<li><a href="https://github.com/clap-rs/clap/commit/806f634bfa777fd7f0966d73d79a31e576fe9bf3"><code>806f634</code></a> chore: trim Cargo manifest fields whose values are defaults</li>
<li>Additional commits viewable in <a href="https://github.com/clap-rs/clap/compare/v3.2.5...v3.2.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=3.2.5&new-version=3.2.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2a30e63..092df44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -136,6 +136,7 @@ dependencies = [
  "textwrap",
  "thiserror",
  "tokio",
+ "toml",
  "toml_edit",
  "tracing",
  "tracing-subscriber",
@@ -173,9 +174,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "clap"
-version = "3.2.5"
+version = "3.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
+checksum = "9f1fe12880bae935d142c8702d500c63a4e8634b6c3c57ad72bf978fc7b6249a"
 dependencies = [
  "atty",
  "bitflags",
@@ -200,9 +201,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "3.2.5"
+version = "3.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
+checksum = "ed6db9e867166a43a53f7199b5e4d1f522a1e5bd626654be263c999ce59df39a"
 dependencies = [
  "heck",
  "proc-macro-error",
@@ -721,9 +722,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "miette"
-version = "4.7.1"
+version = "5.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c"
+checksum = "6ec753a43fd71bb5f28751c9ec17fbe89d6d26ca8282d1e1f82f5ac3dbd5581e"
 dependencies = [
  "atty",
  "backtrace",
@@ -741,9 +742,9 @@ dependencies = [
 
 [[package]]
 name = "miette-derive"
-version = "4.7.1"
+version = "5.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c"
+checksum = "fdfc33ea15c5446600f91d319299dd40301614afff7143cdfa9bf4c09da3ca64"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1383,6 +1384,15 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "toml_edit"
 version = "0.14.4"
diff --git a/Cargo.toml b/Cargo.toml
index 6a047fa..1109530 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,7 +18,7 @@ exclude = [
 
 [dependencies]
 cargo_metadata = "0.14.2"
-clap = { version = "3.2.5", features = ["derive"] }
+clap = { version = "3.2.6", features = ["derive"] }
 clap-cargo = "0.8.0"
 console = "0.15.0"
 crates-index = { version = "0.18.8", default-features = false }
@@ -39,9 +39,10 @@ toml_edit = { version = "0.14.4", features = ["serde"] }
 tokio = { version = "1.12.0", features = ["fs", "macros", "process", "rt-multi-thread"] }
 tracing = { version = "0.1.34", features = ["log"] }
 tracing-subscriber = "0.3.11"
-miette = { version = "4.7.1", features = ["fancy"] }
+miette = { version = "5.1.0", features = ["fancy"] }
 thiserror = "1.0.31"
 url = "2.2.2"
+toml = "0.5.9"
 
 [target.'cfg(windows)'.dependencies.winapi]
 version = "0.3"
diff --git a/src/errors.rs b/src/errors.rs
index 933e530..c74f255 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,7 +1,7 @@
 use std::{ffi::OsString, fmt::Display, path::PathBuf, string::FromUtf8Error, sync::Arc};
 
 use cargo_metadata::Version;
-use miette::{Diagnostic, NamedSource};
+use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
 use thiserror::Error;
 
 use crate::format::{ImportName, PackageName};
@@ -43,7 +43,7 @@ pub struct NeedsAuditAsErrors {
 
 impl Display for NeedsAuditAsErrors {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str("Some first-party packages match published crates.io versions")?;
+        f.write_str("Some non-crates.io-fetched packages match published crates.io versions")?;
         for e in &self.errors {
             f.write_fmt(format_args!("\n  {}", e))?
         }
@@ -114,11 +114,11 @@ pub enum CertifyError {
 #[non_exhaustive]
 pub enum EditError {
     #[error("Failed to launch editor")]
-    CouldntLaunch(#[diagnostic(cause)] std::io::Error),
+    CouldntLaunch(#[source] std::io::Error),
     #[error("Failed to open result of editor")]
-    CouldntOpen(#[diagnostic(cause)] std::io::Error),
+    CouldntOpen(#[source] std::io::Error),
     #[error("Failed to read result of editor")]
-    CouldntRead(#[diagnostic(cause)] std::io::Error),
+    CouldntRead(#[source] std::io::Error),
 }
 
 ///////////////////////////////////////////////////////////
@@ -129,8 +129,8 @@ pub enum EditError {
 #[error("Failed to initialize the cargo-vet store (supply-chain)")]
 #[non_exhaustive]
 pub enum InitError {
-    StoreCreate(#[diagnostic(cause)] StoreCreateError),
-    StoreCommit(#[diagnostic(cause)] StoreCommitError),
+    StoreCreate(#[source] StoreCreateError),
+    StoreCommit(#[source] StoreCommitError),
 }
 
 //////////////////////////////////////////////////////////
@@ -155,10 +155,10 @@ pub enum MinimizeUnauditedError {
 #[non_exhaustive]
 #[error("Couldn't create the store (supply-chain)")]
 pub enum StoreCreateError {
-    CouldntCreate(#[diagnostic(cause)] std::io::Error),
+    CouldntCreate(#[source] std::io::Error),
     CouldntAcquire(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
 }
@@ -169,19 +169,16 @@ pub enum StoreAcquireError {
     #[error("Couldn't acquire the store's (supply-chain's) lock")]
     CouldntLock(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
     #[error(transparent)]
-    LoadToml(
-        #[from]
-        #[diagnostic(cause)]
-        LoadTomlError,
-    ),
-    #[error(transparent)]
+    #[diagnostic(transparent)]
+    LoadToml(#[from] LoadTomlError),
+    #[error("Couldn't acquire the store")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     #[error(transparent)]
@@ -189,33 +186,53 @@ pub enum StoreAcquireError {
     Validate(#[from] StoreValidateErrors),
 }
 
-#[derive(Debug, Error, Diagnostic)]
-#[error("Your cargo-vet store (supply-chain) has consistency errors")]
-pub struct StoreValidateErrors {
-    #[related]
-    errors: Vec<StoreValidateError>,
-}
-
-#[derive(Debug, Error, Diagnostic)]
-#[non_exhaustive]
-pub enum StoreValidateError {}
-
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 #[error("Failed to commit store")]
 pub enum StoreCommitError {
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     StoreToml(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreTomlError,
     ),
 }
 
+////////////////////////////////////////////////////////////
+// StoreValidateErrors
+////////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Your cargo-vet store (supply-chain) has consistency errors")]
+pub struct StoreValidateErrors {
+    #[related]
+    pub errors: Vec<StoreValidateError>,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreValidateError {
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    InvalidCriteria(InvalidCriteriaError),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("'{invalid}' is not a valid criteria name")]
+#[diagnostic(help("the possible criteria are {:?}", valid_names))]
+pub struct InvalidCriteriaError {
+    #[source_code]
+    pub source_code: SourceFile,
+    #[label]
+    pub span: SourceSpan,
+    pub invalid: String,
+    pub valid_names: Arc<Vec<String>>,
+}
+
 //////////////////////////////////////////////////////////
 // CacheErrors
 /////////////////////////////////////////////////////////
@@ -223,40 +240,40 @@ pub enum StoreCommitError {
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 pub enum CacheAcquireError {
-    #[error(transparent)]
+    #[error("Couldn't acquire cache")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     #[error("Failed to create cache root dir: {}", target.display())]
     Root {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache src dir: {}", target.display())]
     Src {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache empty dir: {}", target.display())]
     Empty {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache package dir: {}", target.display())]
     Cache {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Couldn't acquire the cache's lock")]
     CouldntLock(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
 }
@@ -267,17 +284,17 @@ pub enum CacheAcquireError {
 pub enum CacheCommitError {
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     StoreToml(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreTomlError,
     ),
     StoreJson(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreJsonError,
     ),
 }
@@ -290,11 +307,11 @@ pub enum CacheCommitError {
 #[non_exhaustive]
 pub enum CommandError {
     #[error("Command failed")]
-    CommandFailed(#[diagnostic(cause)] std::io::Error),
+    CommandFailed(#[source] std::io::Error),
     #[error("Bad status {0}")]
     BadStatus(i32),
     #[error("Wasn't UTF-8")]
-    BadOutput(#[diagnostic(cause)] FromUtf8Error),
+    BadOutput(#[source] FromUtf8Error),
 }
 
 //////////////////////////////////////////////////////////
@@ -322,7 +339,7 @@ pub enum DiffError {
     #[error("Failed to diff package")]
     CommandError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         CommandError,
     ),
 }
@@ -335,9 +352,9 @@ pub enum DiffError {
 #[non_exhaustive]
 pub enum UserInfoError {
     #[error("Failed to get user.name")]
-    UserCommandFailed(#[diagnostic(cause)] CommandError),
+    UserCommandFailed(#[source] CommandError),
     #[error("Failed to get user.email")]
-    EmailCommandFailed(#[diagnostic(cause)] CommandError),
+    EmailCommandFailed(#[source] CommandError),
 }
 
 //////////////////////////////////////////////////////////
@@ -350,7 +367,7 @@ pub enum FetchError {
     #[error("Invalid URL for package: {url}")]
     InvalidUrl {
         url: String,
-        #[diagnostic(cause)]
+        #[source]
         error: url::ParseError,
     },
     #[error("Running as --frozen but needed to fetch {package}:{version}")]
@@ -361,13 +378,13 @@ pub enum FetchError {
     #[error("Failed to unpack .crate at {}", src.display())]
     Unpack {
         src: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: UnpackError,
     },
     #[error("failed to open cached .crate at {}", target.display())]
     OpenCached {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error(transparent)]
@@ -379,9 +396,9 @@ pub enum FetchError {
 #[non_exhaustive]
 pub enum UnpackError {
     #[error("Failed to iterate archive")]
-    ArchiveIterate(#[diagnostic(cause)] std::io::Error),
+    ArchiveIterate(#[source] std::io::Error),
     #[error("Failed to read archive entry")]
-    ArchiveEntry(#[diagnostic(cause)] std::io::Error),
+    ArchiveEntry(#[source] std::io::Error),
     #[error("Invalid archive, {} wasn't under {}", entry_path.display(), prefix.to_string_lossy())]
     InvalidPaths {
         entry_path: PathBuf,
@@ -390,13 +407,13 @@ pub enum UnpackError {
     #[error("Failed to unpack archive entry {}", entry_path.display())]
     Unpack {
         entry_path: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to finalize unpack to {}", target.display())]
     LockCreate {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error(transparent)]
@@ -415,7 +432,7 @@ pub enum FetchAuditError {
     InvalidUrl {
         import_name: ImportName,
         import_url: String,
-        #[diagnostic(cause)]
+        #[source]
         error: url::ParseError,
     },
     #[diagnostic(transparent)]
@@ -423,7 +440,7 @@ pub enum FetchAuditError {
     Download(#[from] DownloadError),
     #[diagnostic(transparent)]
     #[error(transparent)]
-    Toml(#[from] TomlParseError),
+    Toml(#[from] LoadTomlError),
     #[diagnostic(transparent)]
     #[error(transparent)]
     Validate(#[from] StoreValidateErrors),
@@ -439,37 +456,37 @@ pub enum DownloadError {
     #[error("failed to start download of {url}")]
     FailedToStartDownload {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: reqwest::Error,
     },
     #[error("failed to create file for download to {}", target.display())]
     FailedToCreateDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("failed to read download from {url}")]
     FailedToReadDownload {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: reqwest::Error,
     },
     #[error("failed to write download to {}", target.display())]
     FailedToWriteDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("failed to rename download to final location {}", target.display())]
     FailedToFinalizeDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Download wasn't valid utf8: {url}")]
     InvalidText {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: FromUtf8Error,
     },
 }
@@ -493,10 +510,10 @@ pub enum SuggestError {
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 pub enum FlockError {
-    #[error(transparent)]
+    #[error("couldn't acquire file lock")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -508,12 +525,12 @@ pub enum FlockError {
 #[derive(Debug, Error, Diagnostic)]
 #[error("Failed to parse toml file")]
 pub struct TomlParseError {
-    // #[source_code]
-    // input: SourceFile,
-    // #[label("here")]
-    // span: SourceOffset,
-    #[diagnostic(cause)]
-    pub error: toml_edit::de::Error,
+    #[source_code]
+    pub source_code: SourceFile,
+    #[label("here")]
+    pub span: SourceOffset,
+    #[source]
+    pub error: toml::de::Error,
 }
 
 #[derive(Debug, Error, Diagnostic)]
@@ -523,7 +540,7 @@ pub struct JsonParseError {
     // input: SourceFile,
     // #[label("here")]
     // span: SourceOffset,
-    #[diagnostic(cause)]
+    #[source]
     pub error: serde_json::Error,
 }
 
@@ -533,10 +550,18 @@ pub enum LoadTomlError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     TomlParse(#[from] TomlParseError),
-    #[error(transparent)]
+
+    #[error("TOML wasn't valid utf8")]
+    InvalidText {
+        #[source]
+        #[from]
+        error: FromUtf8Error,
+    },
+
+    #[error("couldn't load toml")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -547,10 +572,10 @@ pub enum LoadJsonError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     JsonParse(#[from] JsonParseError),
-    #[error(transparent)]
+    #[error("couldn't load json")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -560,10 +585,10 @@ pub enum LoadJsonError {
 pub enum StoreJsonError {
     #[error(transparent)]
     JsonSerialize(#[from] serde_json::Error),
-    #[error(transparent)]
+    #[error("couldn't store json")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -573,10 +598,10 @@ pub enum StoreJsonError {
 pub enum StoreTomlError {
     #[error(transparent)]
     TomlSerialize(#[from] toml_edit::ser::Error),
-    #[error(transparent)]
+    #[error("couldn't store toml")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
diff --git a/src/format.rs b/src/format.rs
index 86fec02..2ae2276 100644
--- a/src/format.rs
+++ b/src/format.rs
@@ -1,5 +1,6 @@
 //! Details of the file formats used by cargo vet
 
+use crate::serialization::spanned::Spanned;
 use crate::{flock::Filesystem, serialization};
 use core::{cmp, fmt};
 use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
@@ -149,17 +150,16 @@ pub struct CriteriaEntry {
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec")]
-    pub implies: Vec<CriteriaName>,
+    pub implies: Vec<Spanned<CriteriaName>>,
 }
 
 /// This is conceptually an enum
 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(try_from = "serialization::audit::AuditEntryAll")]
+#[serde(into = "serialization::audit::AuditEntryAll")]
 pub struct AuditEntry {
     pub who: Option<String>,
-    #[serde(default)]
-    #[serde(with = "serialization::string_or_vec")]
-    pub criteria: Vec<CriteriaName>,
-    #[serde(flatten)]
+    pub criteria: Vec<Spanned<CriteriaName>>,
     pub kind: AuditKind,
     pub notes: Option<String>,
 }
@@ -179,23 +179,14 @@ impl cmp::Ord for AuditEntry {
     }
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)]
-#[serde(untagged)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
 pub enum AuditKind {
     Full {
         version: Version,
-        #[serde(rename = "dependency-criteria")]
-        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
-        #[serde(with = "serialization::dependency_criteria")]
-        #[serde(default)]
         dependency_criteria: DependencyCriteria,
     },
     Delta {
         delta: Delta,
-        #[serde(rename = "dependency-criteria")]
-        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
-        #[serde(with = "serialization::dependency_criteria")]
-        #[serde(default)]
         dependency_criteria: DependencyCriteria,
     },
     Violation {
@@ -211,7 +202,7 @@ pub enum AuditKind {
 /// ```toml
 /// dependency_criteria = { hmac: ['secure', 'crypto_reviewed'] }
 /// ```
-pub type DependencyCriteria = SortedMap<PackageName, Vec<CriteriaName>>;
+pub type DependencyCriteria = SortedMap<PackageName, Vec<Spanned<CriteriaName>>>;
 
 /// A "VERSION -> VERSION"
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -355,14 +346,14 @@ pub struct PolicyEntry {
     /// If not present, this defaults to the default criteria in the audits table.
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec_or_none")]
-    pub criteria: Option<Vec<CriteriaName>>,
+    pub criteria: Option<Vec<Spanned<CriteriaName>>>,
 
     /// Same as `criteria`, but for first-party(?) crates/dependencies that are only
     /// used as dev-dependencies.
     #[serde(rename = "dev-criteria")]
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec_or_none")]
-    pub dev_criteria: Option<Vec<CriteriaName>>,
+    pub dev_criteria: Option<Vec<Spanned<CriteriaName>>>,
 
     /// TODO: figure this out
     pub targets: Option<Vec<String>>,
@@ -404,7 +395,7 @@ pub struct CriteriaMapping {
     pub ours: CriteriaName,
     /// If all of these foreign criteria apply
     #[serde(with = "serialization::string_or_vec")]
-    pub theirs: Vec<ForeignCriteriaName>,
+    pub theirs: Vec<Spanned<ForeignCriteriaName>>,
 }
 
 /// Semantically identical to a 'full audit' entry, but private to our project
@@ -418,7 +409,7 @@ pub struct UnauditedDependency {
     /// pick a "good" initial value.
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec")]
-    pub criteria: Vec<CriteriaName>,
+    pub criteria: Vec<Spanned<CriteriaName>>,
     /// Whether 'suggest' should bother mentioning this (defaults true).
     #[serde(default = "get_default_unaudited_suggest")]
     #[serde(skip_serializing_if = "is_default_unaudited_suggest")]
diff --git a/src/main.rs b/src/main.rs
index d468bc0..ee5c1d0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -149,7 +149,8 @@ fn main() -> Result<(), ()> {
         }
     };
     main_result.map_err(|e| {
-        eprintln!("{:?}", e);
+        let out: &mut dyn Out = &mut Term::stderr();
+        writeln!(out, "{:?}", e).unwrap();
         std::process::exit(-1);
     })
 }
@@ -212,18 +213,24 @@ fn real_main() -> Result<(), miette::Report> {
     }));
 
     // FIXME: we should have separate configs for errors but this works for now
-    let error_colors_enabled = cli.output_file.is_none();
+    let error_colors_enabled = cli.output_file.is_none() && console::colors_enabled_stderr();
+    let output_json = cli.output_format == OutputFormat::Json;
     miette::set_hook(Box::new(move |_| {
-        let graphical_theme = if error_colors_enabled {
-            miette::GraphicalTheme::unicode()
+        if output_json {
+            let json_handler = miette::JSONReportHandler;
+            Box::new(json_handler)
         } else {
-            miette::GraphicalTheme::unicode_nocolor()
-        };
-        Box::new(
-            miette::MietteHandlerOpts::new()
-                .graphical_theme(graphical_theme)
-                .build(),
-        )
+            let graphical_theme = if error_colors_enabled {
+                miette::GraphicalTheme::unicode()
+            } else {
+                miette::GraphicalTheme::unicode_nocolor()
+            };
+            Box::new(
+                miette::MietteHandlerOpts::new()
+                    .graphical_theme(graphical_theme)
+                    .build(),
+            )
+        }
     }))
     .expect("Failed to initialize error handler");
 
@@ -461,9 +468,9 @@ pub fn init_files(
                 continue;
             }
             let criteria = if package.is_dev_only {
-                vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string()]
+                vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string().into()]
             } else {
-                vec![format::DEFAULT_POLICY_CRITERIA.to_string()]
+                vec![format::DEFAULT_POLICY_CRITERIA.to_string().into()]
             };
             // NOTE: May have multiple copies of a package!
             let item = UnauditedDependency {
@@ -577,7 +584,7 @@ fn do_cmd_certify(
             dep_criteria
                 .entry(arg.dependency.clone())
                 .or_insert_with(Vec::new)
-                .push(arg.criteria.clone());
+                .push(arg.criteria.clone().into());
         }
         dep_criteria
     };
@@ -684,7 +691,7 @@ fn do_cmd_certify(
                     .collect::<Vec<_>>()
             )?;
             writeln!(out, "(press ENTER to accept the current criteria)")?;
-            let input = out.read_line_initial_text("> ")?;
+            let input = out.read_line_with_prompt("> ")?;
             let input = input.trim();
             if input.is_empty() {
                 if chosen_criteria.is_empty() {
@@ -809,7 +816,10 @@ fn do_cmd_certify(
 
     let new_entry = AuditEntry {
         kind: kind.clone(),
-        criteria: criteria_names.iter().map(|s| s.to_string()).collect(),
+        criteria: criteria_names
+            .iter()
+            .map(|s| s.to_string().into())
+            .collect(),
         who,
         notes,
     };
@@ -918,9 +928,13 @@ fn cmd_record_violation(
 
     let criteria = if sub_args.criteria.is_empty() {
         // TODO: provide an interactive prompt for this
-        vec![store.config.default_criteria.clone()]
+        vec![store.config.default_criteria.clone().into()]
     } else {
-        sub_args.criteria.clone()
+        sub_args
+            .criteria
+            .iter()
+            .map(|s| s.to_owned().into())
+            .collect()
     };
 
     // FIXME: can/should we check if the version makes sense..?
@@ -974,7 +988,7 @@ fn cmd_add_unaudited(
             dep_criteria
                 .entry(arg.dependency.clone())
                 .or_insert_with(Vec::new)
-                .push(arg.criteria.clone());
+                .push(arg.criteria.clone().into());
         }
         dep_criteria
     };
@@ -983,9 +997,13 @@ fn cmd_add_unaudited(
 
     let criteria = if sub_args.criteria.is_empty() {
         // TODO: provide an interactive prompt for this
-        vec![store.config.default_criteria.clone()]
+        vec![store.config.default_criteria.clone().into()]
     } else {
-        sub_args.criteria.clone()
+        sub_args
+            .criteria
+            .iter()
+            .map(|s| s.to_owned().into())
+            .collect()
     };
 
     let suggest = !sub_args.no_suggest;
@@ -1154,7 +1172,7 @@ pub fn minimize_unaudited(
                     .or_insert(Vec::new())
                     .push(UnauditedDependency {
                         version: item.suggested_diff.to.clone(),
-                        criteria: criteria_names,
+                        criteria: criteria_names.iter().map(|s| s.to_owned().into()).collect(),
                         dependency_criteria: DependencyCriteria::new(),
                         notes: None,
                         suggest: true,
@@ -1242,7 +1260,14 @@ fn cmd_vet(out: &mut dyn Out, cfg: &Config) -> Result<(), miette::Report> {
             ResolveDepth::Deep
         },
     );
-    let suggest = report.compute_suggest(cfg, network.as_ref(), true)?;
+
+    // Bare `cargo vet` shouldn't suggest in CI
+    let suggest = if !cfg.cli.locked {
+        report.compute_suggest(cfg, network.as_ref(), true)?
+    } else {
+        None
+    };
+
     match cfg.cli.output_format {
         OutputFormat::Human => report
             .print_human(out, cfg, suggest.as_ref())
diff --git a/src/out.rs b/src/out.rs
index 8ecda3c..1f57091 100644
--- a/src/out.rs
+++ b/src/out.rs
@@ -25,7 +25,7 @@ pub trait Out: io::Write {
 
     /// Ask the user a question, and read in a line with the user's response. If
     /// there's no user able to respond, `None` will be returned instead.
-    fn read_line_initial_text(&mut self, _initial: &str) -> io::Result<String> {
+    fn read_line_with_prompt(&mut self, _prompt: &str) -> io::Result<String> {
         Err(io::ErrorKind::Unsupported.into())
     }
 
@@ -53,8 +53,10 @@ impl Out for Term {
         (&*self).clear_screen()
     }
 
-    fn read_line_initial_text(&mut self, initial: &str) -> io::Result<String> {
-        (&*self).read_line_initial_text(initial)
+    fn read_line_with_prompt(&mut self, prompt: &str) -> io::Result<String> {
+        self.write_str(prompt)?;
+        self.flush()?;
+        (&*self).read_line()
     }
 
     fn style(&self) -> Style {
diff --git a/src/resolver.rs b/src/resolver.rs
index 68e4f93..6119476 100644
--- a/src/resolver.rs
+++ b/src/resolver.rs
@@ -33,12 +33,15 @@
 //!         * as with third-parties, this is done per-criteria so we can granularly blame deps
 //!     * if there is a policy.dependency_criteria, then that dep isn't inherited normally
 //!       and is instead effectively no_criteria or all_criteria based on whether it passes or not
-//!     * if there is a policy.criteria (or it's a root), then we check our inherited criteria
-//!       against that policy and then set ourselves to all_criteria or no_criteria
-//!         * **This is the check that matters!** Anything that fails this check is registered
-//!           as a "root (policy) failure" and will be fed into the blame phase.
 //!
-//! * resolve_first_party_dev: same as above, but check dev-deps and dev-policies
+//! * resolve_self_policy: if there is a policy.criteria (or it's a root), then we check
+//!   the resolved criteria against that policy
+//!     * on success, we set ourselves to all_criteria
+//!     * on failure, we set ourselves to no_criteria
+//!     * **This is the check that matters!** Anything that fails this check is registered
+//!       as a "root (policy) failure" and will be fed into the blame phase.
+//!
+//! * resolve_dev: same as above, but check dev-deps and dev-policies
 //!     * this must be done as a second pass because dev-deps can introduce cycles. by doing
 //!       all other analysis first, we can guarantee all dev-deps are fully resolved, as you
 //!       cannot actually depend on the "dev" build of a package.
@@ -74,6 +77,7 @@ use crate::format::{
 use crate::format::{FastMap, FastSet, SortedMap, SortedSet};
 use crate::network::Network;
 use crate::out::Out;
+use crate::serialization::spanned::Spanned;
 use crate::{
     AuditEntry, Cache, Config, CriteriaEntry, DumpGraphArgs, GraphFilter, GraphFilterProperty,
     GraphFilterQuery, PackageExt, Store,
@@ -112,8 +116,6 @@ pub struct Success {
     pub vetted_partially: Vec<PackageIdx>,
     /// Third-party packages that were successfully vetted using only 'audits'
     pub vetted_fully: Vec<PackageIdx>,
-    /// Third-party packages that have an unaudited entry that can be removed.
-    pub useless_unaudited: Vec<PackageIdx>,
 }
 
 #[derive(Debug, Clone)]
@@ -245,7 +247,6 @@ fn pkgid_unstable(pkgid: &PackageId) -> bool {
 pub struct DepGraph<'a> {
     pub nodes: Vec<PackageNode<'a>>,
     pub interner_by_pkgid: SortedMap<&'a PackageId, PackageIdx>,
-    pub interner_by_name_and_ver: SortedMap<PackageStr<'a>, SortedMap<&'a Version, PackageIdx>>,
     pub topo_index: Vec<PackageIdx>,
 }
 
@@ -362,7 +363,7 @@ fn builtin_criteria() -> SortedMap<CriteriaName, CriteriaEntry> {
             CriteriaEntry {
                 description: Some("safe to deploy to production".to_string()),
                 description_url: None,
-                implies: vec!["safe-to-run".to_string()],
+                implies: vec!["safe-to-run".to_string().into()],
             },
         ),
     ]
@@ -395,7 +396,7 @@ impl CriteriaMapper {
 
             fn recursive_implies(
                 result: &mut CriteriaSet,
-                implies: &[CriteriaName],
+                implies: &[Spanned<CriteriaName>],
                 index: &FastMap<CriteriaName, usize>,
                 list: &[(CriteriaName, CriteriaEntry)],
             ) {
@@ -682,8 +683,6 @@ impl<'a> DepGraph<'a> {
         // Do a first-pass where we populate skeletons of the primary nodes
         // and setup the interners, which will only ever refer to these nodes
         let mut interner_by_pkgid = SortedMap::<&PackageId, PackageIdx>::new();
-        let mut interner_by_name_and_ver =
-            SortedMap::<PackageStr, SortedMap<&Version, PackageIdx>>::new();
         let mut nodes = vec![];
 
         // Stub out the initial state of all the nodes
@@ -714,11 +713,6 @@ impl<'a> DepGraph<'a> {
         // Populate the interners based on the new ordering
         for (idx, node) in nodes.iter_mut().enumerate() {
             assert!(interner_by_pkgid.insert(node.package_id, idx).is_none());
-            assert!(interner_by_name_and_ver
-                .entry(node.name)
-                .or_default()
-                .insert(node.version, idx)
-                .is_none());
         }
 
         // Do topological sort: just recursively visit all of a node's children, and only add it
@@ -870,7 +864,6 @@ impl<'a> DepGraph<'a> {
 
         let result = Self {
             interner_by_pkgid,
-            interner_by_name_and_ver,
             nodes,
             topo_index,
         };
@@ -955,7 +948,6 @@ impl<'a> DepGraph<'a> {
         let mut old_to_new = FastMap::new();
         let mut nodes = Vec::new();
         let mut interner_by_pkgid = SortedMap::new();
-        let mut interner_by_name_and_ver = SortedMap::<_, SortedMap<_, _>>::new();
         let mut topo_index = Vec::new();
         for (old_idx, package) in self.nodes.iter().enumerate() {
             if !reachable.contains_key(&old_idx) {
@@ -979,10 +971,6 @@ impl<'a> DepGraph<'a> {
                 is_dev_only: package.is_dev_only,
             });
             interner_by_pkgid.insert(package.package_id, new_idx);
-            interner_by_name_and_ver
-                .entry(package.name)
-                .or_default()
-                .insert(package.version, new_idx);
         }
         for old_idx in &self.topo_index {
             if let Some(&new_idx) = old_to_new.get(old_idx) {
@@ -1028,7 +1016,6 @@ impl<'a> DepGraph<'a> {
         Self {
             nodes,
             interner_by_pkgid,
-            interner_by_name_and_ver,
             topo_index,
         }
     }
@@ -1165,9 +1152,10 @@ pub fn resolve<'a>(
     for &pkgidx in &graph.topo_index {
         let package = &graph.nodes[pkgidx];
 
+        trace!("resolving {}:{}", package.name, package.version,);
+
         if package.is_third_party {
             resolve_third_party(
-                metadata,
                 store,
                 &graph,
                 &criteria_mapper,
@@ -1178,7 +1166,6 @@ pub fn resolve<'a>(
             );
         } else {
             resolve_first_party(
-                metadata,
                 store,
                 &graph,
                 &criteria_mapper,
@@ -1188,6 +1175,17 @@ pub fn resolve<'a>(
                 pkgidx,
             );
         }
+
+        // Check that any policy on our resolved value is satisfied
+        resolve_self_policy(
+            store,
+            &graph,
+            &criteria_mapper,
+            &mut results,
+            &mut violations,
+            &mut root_failures,
+            pkgidx,
+        );
     }
 
     // Now that we've processed the "normal" graph we need to check the dev (test/bench) builds.
@@ -1217,9 +1215,9 @@ pub fn resolve<'a>(
     //   check against root policies.
     for &pkgidx in &graph.topo_index {
         let package = &graph.nodes[pkgidx];
+        trace!("resolving dev {}:{}", package.name, package.version,);
         if package.is_workspace_member {
             resolve_dev(
-                metadata,
                 store,
                 &graph,
                 &criteria_mapper,
@@ -1311,7 +1309,6 @@ pub fn resolve<'a>(
     let mut vetted_with_unaudited = vec![];
     let mut vetted_partially = vec![];
     let mut vetted_fully = vec![];
-    let mut useless_unaudited = vec![];
     for &pkgidx in &graph.topo_index {
         let package = &graph.nodes[pkgidx];
         if !package.is_third_party {
@@ -1327,10 +1324,6 @@ pub fn resolve<'a>(
         } else {
             vetted_partially.push(pkgidx);
         }
-
-        if result.directly_unaudited && !result.needed_unaudited {
-            useless_unaudited.push(pkgidx);
-        }
     }
 
     ResolveReport {
@@ -1341,14 +1334,11 @@ pub fn resolve<'a>(
             vetted_with_unaudited,
             vetted_partially,
             vetted_fully,
-            useless_unaudited,
         }),
     }
 }
 
-#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
 fn resolve_third_party<'a>(
-    _metadata: &'a Metadata,
     store: &'a Store,
     graph: &DepGraph<'a>,
     criteria_mapper: &CriteriaMapper,
@@ -1358,10 +1348,6 @@ fn resolve_third_party<'a>(
     pkgidx: PackageIdx,
 ) {
     let package = &graph.nodes[pkgidx];
-    assert!(
-        package.dev_deps.is_empty(),
-        "third-party packages shouldn't have dev-deps!"
-    );
     let unaudited = store.config.unaudited.get(package.name);
 
     let own_audits = store.audits.audits.get(package.name).unwrap_or(&NO_AUDITS);
@@ -1738,6 +1724,13 @@ fn resolve_third_party<'a>(
         }
     }
 
+    trace!(
+        "  third-party validation: {:?}",
+        criteria_mapper
+            .criteria_names(&validated_criteria)
+            .collect::<Vec<_>>()
+    );
+
     // We've completed our graph analysis for this package, now record the results
     results[pkgidx] = ResolveResult {
         validated_criteria,
@@ -1913,15 +1906,13 @@ fn search_for_path<'a>(
     }
 }
 
-#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
 fn resolve_first_party<'a>(
-    _metadata: &'a Metadata,
     store: &'a Store,
     graph: &DepGraph<'a>,
     criteria_mapper: &CriteriaMapper,
     results: &mut [ResolveResult<'a>],
     _violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
-    root_failures: &mut RootFailures,
+    _root_failures: &mut RootFailures,
     pkgidx: PackageIdx,
 ) {
     // Check the build-deps and normal-deps of this package. dev-deps are checking in `resolve_dep`
@@ -1975,39 +1966,63 @@ fn resolve_first_party<'a>(
         }
     }
     trace!(
-        " first-party {}:{} initial validation: {:?}",
-        package.name,
-        package.version,
+        "  first-party validation: {:?}",
         criteria_mapper
             .criteria_names(&validated_criteria)
             .collect::<Vec<_>>()
     );
+
     // Save the results
     results[pkgidx].search_results = search_results;
     results[pkgidx].validated_criteria = validated_criteria;
+}
+
+fn resolve_self_policy<'a>(
+    store: &'a Store,
+    graph: &DepGraph<'a>,
+    criteria_mapper: &CriteriaMapper,
+    results: &mut [ResolveResult<'a>],
+    _violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
+    root_failures: &mut RootFailures,
+    pkgidx: PackageIdx,
+) {
+    let package = &graph.nodes[pkgidx];
 
     // Now check that we pass our own policy
     let entry = store.config.policy.get(package.name);
     let own_policy = if let Some(c) = entry.and_then(|p| p.criteria.as_ref()) {
+        trace!("  explicit policy: {:?}", c);
         criteria_mapper.criteria_from_list(c)
     } else if package.is_root {
+        trace!("  root policy: {:?}", [format::DEFAULT_POLICY_CRITERIA]);
         criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_CRITERIA])
     } else {
-        trace!("   has no policy, done");
+        trace!("  has no policy, done");
         // We have no policy, we're done
         return;
     };
 
     let mut policy_failures = PolicyFailures::new();
     for criteria_idx in own_policy.indices() {
-        if let SearchResult::PossiblyConnected { failed_deps } =
-            &results[pkgidx].search_results[criteria_idx]
-        {
-            for (&dep, failed_criteria) in failed_deps {
+        match &results[pkgidx].search_results[criteria_idx] {
+            SearchResult::PossiblyConnected { failed_deps } => {
+                // Our children failed us
+                for (&dep, failed_criteria) in failed_deps {
+                    policy_failures
+                        .entry(dep)
+                        .or_insert_with(|| criteria_mapper.no_criteria())
+                        .unioned_with(failed_criteria);
+                }
+            }
+            SearchResult::Disconnected { .. } => {
+                // We failed ourselves
                 policy_failures
-                    .entry(dep)
+                    .entry(pkgidx)
                     .or_insert_with(|| criteria_mapper.no_criteria())
-                    .unioned_with(failed_criteria);
+                    .set_criteria(criteria_idx)
+            }
+            SearchResult::Connected { .. } => {
+                // A-OK
             }
         }
     }
@@ -2016,19 +2031,17 @@ fn resolve_first_party<'a>(
         // We had a policy and it passed, so now we're validated for all criteria
         // because our parents can never require anything else of us. No need
         // to update search_results, they'll be masked out by validated_criteria(?)
-        trace!("   passed policy");
+        trace!("  passed policy, all_criteria");
         results[pkgidx].validated_criteria = criteria_mapper.all_criteria();
     } else {
         // We had a policy and it failed, so now we're invalid for all criteria(?)
-        trace!("   failed policy");
+        trace!("  failed policy, no_criteria");
         results[pkgidx].validated_criteria = criteria_mapper.no_criteria();
         root_failures.push((pkgidx, policy_failures, false));
     }
 }
 
-#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
 fn resolve_dev<'a>(
-    _metadata: &'a Metadata,
     store: &'a Store,
     graph: &DepGraph<'a>,
     criteria_mapper: &CriteriaMapper,
@@ -2088,9 +2101,7 @@ fn resolve_dev<'a>(
         }
     }
     trace!(
-        " first-party dev {}:{} initial validation: {:?}",
-        package.name,
-        package.version,
+        "  dev validation: {:?}",
         criteria_mapper
             .criteria_names(&validated_criteria)
             .collect::<Vec<_>>()
@@ -2103,27 +2114,42 @@ fn resolve_dev<'a>(
     // Now check that we pass our own policy
     let entry = store.config.policy.get(package.name);
     let own_policy = if let Some(c) = entry.and_then(|p| p.dev_criteria.as_ref()) {
+        trace!("  explicit policy: {:?}", c);
         criteria_mapper.criteria_from_list(c)
     } else {
+        trace!("  root policy: {:?}", [format::DEFAULT_POLICY_DEV_CRITERIA]);
         criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_DEV_CRITERIA])
     };
 
     let mut policy_failures = PolicyFailures::new();
     for criteria_idx in own_policy.indices() {
-        if let SearchResult::PossiblyConnected { failed_deps } = &search_results[criteria_idx] {
-            for (&dep, failed_criteria) in failed_deps {
+        match &search_results[criteria_idx] {
+            SearchResult::PossiblyConnected { failed_deps } => {
+                // Our children failed us
+                for (&dep, failed_criteria) in failed_deps {
+                    policy_failures
+                        .entry(dep)
+                        .or_insert_with(|| criteria_mapper.no_criteria())
+                        .unioned_with(failed_criteria);
+                }
+            }
+            SearchResult::Disconnected { .. } => {
+                // We failed ourselves
                 policy_failures
-                    .entry(dep)
+                    .entry(pkgidx)
                     .or_insert_with(|| criteria_mapper.no_criteria())
-                    .unioned_with(failed_criteria);
+                    .set_criteria(criteria_idx)
+            }
+            SearchResult::Connected { .. } => {
+                // A-OK
             }
         }
     }
 
     if policy_failures.is_empty() {
-        trace!("   passed dev policy");
+        trace!("  passed dev policy");
     } else {
-        trace!("   failed dev policy");
+        trace!("  failed dev policy");
         root_failures.push((pkgidx, policy_failures, true));
     }
 }
@@ -2314,13 +2340,7 @@ impl<'a> ResolveReport<'a> {
     }
 
     pub fn _has_warnings(&self) -> bool {
-        if let Conclusion::Success(success) = &self.conclusion {
-            // If there are useless_unaudited, we'll warn about those
-            !success.useless_unaudited.is_empty()
-        } else {
-            // Errors aren't warnings
-            false
-        }
+        false
     }
 
     pub fn compute_suggest(
@@ -2615,7 +2635,6 @@ impl<'a> ResolveReport<'a> {
                     "vetted_fully": success.vetted_fully.iter().map(json_package).collect::<Vec<_>>(),
                     "vetted_partially": success.vetted_partially.iter().map(json_package).collect::<Vec<_>>(),
                     "vetted_with_unaudited": success.vetted_with_unaudited.iter().map(json_package).collect::<Vec<_>>(),
-                    "useless_unaudited": success.useless_unaudited.iter().map(json_package).collect::<Vec<_>>(),
                 })
             }
             Conclusion::FailForViolationConflict(fail) => json!({
@@ -2666,7 +2685,7 @@ impl Success {
     pub fn print_human(
         &self,
         out: &mut dyn Out,
-        report: &ResolveReport,
+        _report: &ResolveReport,
         _cfg: &Config,
     ) -> Result<(), std::io::Error> {
         let fully_audited_count = self.vetted_fully.len();
@@ -2711,19 +2730,6 @@ impl Success {
 
             writeln!(out, ")")?;
         }
-
-        // Warn about useless unaudited entries
-        if !self.useless_unaudited.is_empty() {
-            writeln!(
-                out,
-                "  warning: some dependencies are listed in unaudited, but didn't need it:"
-            )?;
-            for &pkgidx in &self.useless_unaudited {
-                let package = &report.graph.nodes[pkgidx];
-                writeln!(out, "    {}:{}", package.name, package.version)?;
-            }
-        }
-
         Ok(())
     }
 }
diff --git a/src/serialization.rs b/src/serialization.rs
index 5e97f7d..64c874e 100644
--- a/src/serialization.rs
+++ b/src/serialization.rs
@@ -6,13 +6,83 @@ use serde::{
     de::{self, value, SeqAccess, Visitor},
     Deserialize, Deserializer, Serialize, Serializer,
 };
+use spanned::Spanned;
 
-/// Serde handler to allow specifying any of [], "foo",
-/// ["foo"], or ["foo", "bar"].
+/// Serde handler to allow specifying any of [], "foo", ["foo"], or ["foo", "bar"],
+/// with the strings getting proper toml spans from the original source. Specifically,
+/// we deserialize `Vec<Spanned<String>>`.
+///
+/// This needs to solve three problems:
+///
+/// 1. Getting spans from toml deserialization, in general
+/// 2. Being able to take two very different inputs and squish them into one form
+/// 3. Wiring the span machinery into that process
+///
+/// Problem 1 is solved by toml-rs having a [Spanned] type for this exact thing.
+/// It's genuinely magic in that the deserializer just checks if it's deserializing
+/// a struct with its exact magic field names and then emits two extra values for the span.
+/// We fork the actual Spanned type here to make it more ergonomic/transparent
+///
+/// Problem 2 is solved by [string_or_vec_inner], which is based on an example in serde's
+/// own docs. We create a custom visitor that can be visited with either a sequence or
+/// a string. In the sequence case we just do a normal deserialization of our target.
+/// In the string case we create a new Vec to wrap our string.
+///
+/// Problem 3 is where things get messy. Our solution to Problem 2 naturally gets spanned
+/// for the sequence case, but in the string case we're "too late" to request spanning,
+/// as we no longer have a deserializer, just a `str`. All we can do is emit a dummy Spanned.
+///
+/// Our solution to this is to recognize that in this case we have an array of one element,
+/// and so a span over the entire array is just as good as a span over the element.
+/// So we deserialize a `Spanned<Vec<Spanned<String>>>` and if the Vec has `len == 1`,
+/// we edit the dummy inner span to equal the outer span. We then just discard the outer span
+/// (or we could keep it if we decide that's useful later).
+///
+/// To make this a bit easier to do, we wrap Problem 2's solution in a `StringOrVec` struct,
+/// deserialize `Spanned<StringOrVec>`, and then transform that into `Vec<Spanned<String>>`
 pub mod string_or_vec {
     use super::*;
 
-    pub fn serialize<S>(v: &Vec<String>, s: S) -> Result<S::Ok, S::Error>
+    /// A type using string_or_vec_inner to make it easy to wrap in Spanned.
+    #[derive(Serialize, Deserialize)]
+    pub struct StringOrVec(
+        #[serde(default)]
+        #[serde(with = "string_or_vec_inner")]
+        pub Vec<Spanned<String>>,
+    );
+
+    pub fn serialize<S>(v: &Vec<Spanned<String>>, s: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        if v.len() == 1 {
+            s.serialize_str(&v[0])
+        } else {
+            v.serialize(s)
+        }
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Spanned<String>>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // Get a Spanned<StringOrVec> and then use it to fixup a dummy span for len == 1
+        let spanned_vec = Spanned::<StringOrVec>::deserialize(deserializer)?;
+        let start = Spanned::start(&spanned_vec);
+        let end = Spanned::end(&spanned_vec);
+        let mut vec = Spanned::into_inner(spanned_vec).0;
+        if vec.len() == 1 {
+            Spanned::update_span(&mut vec[0], start, end);
+        }
+        Ok(vec)
+    }
+}
+
+/// See [string_or_vec]
+pub mod string_or_vec_inner {
+    use super::*;
+
+    pub fn serialize<S>(v: &Vec<Spanned<String>>, s: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
@@ -23,14 +93,14 @@ pub mod string_or_vec {
         }
     }
 
-    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Spanned<String>>, D::Error>
     where
         D: Deserializer<'de>,
     {
         struct StringOrVec;
 
         impl<'de> Visitor<'de> for StringOrVec {
-            type Value = Vec<String>;
+            type Value = Vec<Spanned<String>>;
 
             fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                 formatter.write_str("string or list of strings")
@@ -40,7 +110,7 @@ pub mod string_or_vec {
             where
                 E: de::Error,
             {
-                Ok(vec![s.to_owned()])
+                Ok(vec![Spanned::from(s.to_owned())])
             }
 
             fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
@@ -61,7 +131,7 @@ pub mod string_or_vec {
 pub mod string_or_vec_or_none {
     use super::*;
 
-    pub fn serialize<S>(maybe_v: &Option<Vec<String>>, s: S) -> Result<S::Ok, S::Error>
+    pub fn serialize<S>(maybe_v: &Option<Vec<Spanned<String>>>, s: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
@@ -72,7 +142,7 @@ pub mod string_or_vec_or_none {
         }
     }
 
-    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<Spanned<String>>>, D::Error>
     where
         D: Deserializer<'de>,
     {
@@ -88,7 +158,7 @@ pub mod dependency_criteria {
 
     use super::*;
     #[derive(Serialize, Deserialize)]
-    struct Wrapper(#[serde(with = "string_or_vec")] Vec<CriteriaName>);
+    struct Wrapper(#[serde(with = "string_or_vec")] Vec<Spanned<CriteriaName>>);
 
     pub fn serialize<S>(c: &DependencyCriteria, s: S) -> Result<S::Ok, S::Error>
     where
@@ -105,8 +175,94 @@ pub mod dependency_criteria {
     where
         D: Deserializer<'de>,
     {
-        let m: SortedMap<PackageName, Wrapper> = SortedMap::deserialize(deserializer)?;
-        Ok(m.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
+        let m = SortedMap::<PackageName, Wrapper>::deserialize(deserializer)?;
+        Ok(m.into_iter().map(|(k, v)| (k, v.0)).collect())
+    }
+}
+
+pub mod audit {
+    use super::*;
+
+    use crate::format::{
+        AuditEntry, AuditKind, CriteriaName, Delta, DependencyCriteria, VersionReq,
+    };
+    use cargo_metadata::Version;
+
+    #[derive(Serialize, Deserialize)]
+    pub struct AuditEntryAll {
+        who: Option<String>,
+        #[serde(default)]
+        #[serde(with = "string_or_vec")]
+        criteria: Vec<Spanned<CriteriaName>>,
+        version: Option<Version>,
+        delta: Option<Delta>,
+        violation: Option<VersionReq>,
+        #[serde(rename = "dependency-criteria")]
+        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
+        #[serde(with = "dependency_criteria")]
+        #[serde(default)]
+        dependency_criteria: DependencyCriteria,
+        notes: Option<String>,
+    }
+
+    impl TryFrom<AuditEntryAll> for AuditEntry {
+        type Error = String;
+        fn try_from(val: AuditEntryAll) -> Result<AuditEntry, Self::Error> {
+            let kind = match (val.version, val.delta, val.violation) {
+                (Some(version), None, None) => Ok(AuditKind::Full {
+                    version,
+                    dependency_criteria: val.dependency_criteria,
+                }),
+                (None, Some(delta), None) => Ok(AuditKind::Delta {
+                    delta,
+                    dependency_criteria: val.dependency_criteria,
+                }),
+                (None, None, Some(violation)) => {
+                    if val.dependency_criteria.is_empty() {
+                        Ok(AuditKind::Violation { violation })
+                    } else {
+                        Err("'violation' can't have dependency_criteria".to_string())
+                    }
+                }
+                _ => Err(
+                    "audit entires must have exactly one of 'version', 'delta', and 'violation'"
+                        .to_string(),
+                ),
+            };
+            Ok(AuditEntry {
+                who: val.who,
+                notes: val.notes,
+                criteria: val.criteria,
+                kind: kind?,
+            })
+        }
+    }
+
+    impl From<AuditEntry> for AuditEntryAll {
+        fn from(val: AuditEntry) -> AuditEntryAll {
+            let (version, delta, violation, dependency_criteria) = match val.kind {
+                AuditKind::Full {
+                    version,
+                    dependency_criteria,
+                } => (Some(version), None, None, dependency_criteria),
+                AuditKind::Delta {
+                    delta,
+                    dependency_criteria,
+                } => (None, Some(delta), None, dependency_criteria),
+                AuditKind::Violation { violation } => {
+                    (None, None, Some(violation), DependencyCriteria::new())
+                }
+            };
+            AuditEntryAll {
+                who: val.who,
+                notes: val.notes,
+                criteria: val.criteria,
+                version,
+                delta,
+                violation,
+                dependency_criteria,
+            }
+        }
     }
 }
 
@@ -201,6 +357,202 @@ where
     Ok(toml_document)
 }
 
+pub mod spanned {
+    use std::{
+        borrow::Borrow,
+        cmp::Ordering,
+        fmt::{self, Display},
+        hash::{Hash, Hasher},
+        ops::{Deref, DerefMut},
+    };
+
+    use miette::SourceSpan;
+    use serde::{ser, Deserialize};
+
+    /// A spanned value, indicating the range at which it is defined in the source.
+    #[derive(Clone, Default, Deserialize)]
+    #[serde(from = "toml::Spanned<T>")]
+    pub struct Spanned<T> {
+        start: usize,
+        end: usize,
+        value: T,
+    }
+
+    impl<T> Spanned<T> {
+        /// Access the start of the span of the contained value.
+        pub fn start(this: &Self) -> usize {
+            this.start
+        }
+
+        /// Access the end of the span of the contained value.
+        pub fn end(this: &Self) -> usize {
+            this.end
+        }
+
+        /// Update the span
+        pub fn update_span(this: &mut Self, start: usize, end: usize) {
+            this.start = start;
+            this.end = end;
+        }
+
+        /// Get the span of the contained value.
+        pub fn span(this: &Self) -> SourceSpan {
+            (Self::start(this)..Self::end(this)).into()
+        }
+
+        /// Consumes the spanned value and returns the contained value.
+        pub fn into_inner(this: Self) -> T {
+            this.value
+        }
+    }
+
+    impl<T> IntoIterator for Spanned<T>
+    where
+        T: IntoIterator,
+    {
+        type IntoIter = T::IntoIter;
+        type Item = T::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<'a, T> IntoIterator for &'a Spanned<T>
+    where
+        &'a T: IntoIterator,
+    {
+        type IntoIter = <&'a T as IntoIterator>::IntoIter;
+        type Item = <&'a T as IntoIterator>::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<'a, T> IntoIterator for &'a mut Spanned<T>
+    where
+        &'a mut T: IntoIterator,
+    {
+        type IntoIter = <&'a mut T as IntoIterator>::IntoIter;
+        type Item = <&'a mut T as IntoIterator>::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<T> fmt::Debug for Spanned<T>
+    where
+        T: fmt::Debug,
+    {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            self.value.fmt(f)
+        }
+    }
+
+    impl<T> Display for Spanned<T>
+    where
+        T: Display,
+    {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            self.value.fmt(f)
+        }
+    }
+
+    impl<T> Deref for Spanned<T> {
+        type Target = T;
+        fn deref(&self) -> &Self::Target {
+            &self.value
+        }
+    }
+
+    impl<T> DerefMut for Spanned<T> {
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.value
+        }
+    }
+
+    impl Borrow<str> for Spanned<String> {
+        fn borrow(&self) -> &str {
+            self
+        }
+    }
+
+    impl<T, U: ?Sized> AsRef<U> for Spanned<T>
+    where
+        T: AsRef<U>,
+    {
+        fn as_ref(&self) -> &U {
+            self.value.as_ref()
+        }
+    }
+
+    impl<T: PartialEq> PartialEq for Spanned<T> {
+        fn eq(&self, other: &Self) -> bool {
+            self.value.eq(&other.value)
+        }
+    }
+
+    impl<T: PartialEq<T>> PartialEq<T> for Spanned<T> {
+        fn eq(&self, other: &T) -> bool {
+            self.value.eq(other)
+        }
+    }
+
+    impl<T: Eq> Eq for Spanned<T> {}
+
+    impl<T: Hash> Hash for Spanned<T> {
+        fn hash<H: Hasher>(&self, state: &mut H) {
+            self.value.hash(state);
+        }
+    }
+
+    impl<T: PartialOrd> PartialOrd for Spanned<T> {
+        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+            self.value.partial_cmp(&other.value)
+        }
+    }
+
+    impl<T: PartialOrd<T>> PartialOrd<T> for Spanned<T> {
+        fn partial_cmp(&self, other: &T) -> Option<Ordering> {
+            self.value.partial_cmp(other)
+        }
+    }
+
+    impl<T: Ord> Ord for Spanned<T> {
+        fn cmp(&self, other: &Self) -> Ordering {
+            self.value.cmp(&other.value)
+        }
+    }
+
+    impl<T> From<T> for Spanned<T> {
+        fn from(value: T) -> Self {
+            Self {
+                start: 0,
+                end: 0,
+                value,
+            }
+        }
+    }
+
+    impl<T> From<toml::Spanned<T>> for Spanned<T> {
+        fn from(value: toml::Spanned<T>) -> Self {
+            Self {
+                start: value.start(),
+                end: value.end(),
+                value: value.into_inner(),
+            }
+        }
+    }
+
+    impl<T: ser::Serialize> ser::Serialize for Spanned<T> {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: ser::Serializer,
+        {
+            self.value.serialize(serializer)
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use crate::format::*;
@@ -211,8 +563,8 @@ mod test {
         dc_long.insert(
             "example-crate-1".to_owned(),
             vec![
-                "criteria-one-very-long".to_owned(),
-                "criteria-two-very-long".to_owned(),
+                "criteria-one-very-long".to_owned().into(),
+                "criteria-two-very-long".to_owned().into(),
             ],
         );
         dc_long.insert(
@@ -220,23 +572,25 @@ mod test {
             vec![
                 // This array would wrap over multiple lines if byte length was
                 // used rather than utf-8 character length.
-                "criteria-one-✨✨✨✨✨✨✨✨✨✨".to_owned(),
-                "criteria-two-✨✨✨✨✨✨✨✨✨✨".to_owned(),
+                "criteria-one-✨✨✨✨✨✨✨✨✨✨".to_owned().into(),
+                "criteria-two-✨✨✨✨✨✨✨✨✨✨".to_owned().into(),
             ],
         );
         dc_long.insert(
             "example-crate-3".to_owned(),
             vec![
-                "criteria-one-very-long".to_owned(),
-                "criteria-two-very-long".to_owned(),
-                "criteria-three-extremely-long-this-array-should-wrap".to_owned(),
+                "criteria-one-very-long".to_owned().into(),
+                "criteria-two-very-long".to_owned().into(),
+                "criteria-three-extremely-long-this-array-should-wrap"
+                    .to_owned()
+                    .into(),
             ],
         );
 
         let mut dc_short = SortedMap::new();
         dc_short.insert(
             "example-crate-1".to_owned(),
-            vec!["criteria-one".to_owned()],
+            vec!["criteria-one".to_owned().into()],
         );
 
         let mut audits = SortedMap::new();
@@ -245,7 +599,7 @@ mod test {
             vec![
                 AuditEntry {
                     who: None,
-                    criteria: vec!["long-criteria".to_owned()],
+                    criteria: vec!["long-criteria".to_owned().into()],
                     kind: AuditKind::Full {
                         version: "1.0.0".parse().unwrap(),
                         dependency_criteria: dc_long,
@@ -254,7 +608,7 @@ mod test {
                 },
                 AuditEntry {
                     who: None,
-                    criteria: vec!["short-criteria".to_owned()],
+                    criteria: vec!["short-criteria".to_owned().into()],
                     kind: AuditKind::Full {
                         version: "1.0.0".parse().unwrap(),
                         dependency_criteria: dc_short,
diff --git a/src/storage.rs b/src/storage.rs
index deae943..1464671 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -12,6 +12,7 @@ use cargo_metadata::Version;
 use crates_index::Index;
 use flate2::read::GzDecoder;
 use futures_util::future::try_join_all;
+use miette::{NamedSource, SourceOffset};
 use reqwest::Url;
 use serde::{Deserialize, Serialize};
 use tar::Archive;
@@ -20,18 +21,19 @@ use tracing::{error, info, log::warn, trace};
 use crate::{
     errors::{
         CacheAcquireError, CacheCommitError, CommandError, DiffError, FetchAndDiffError,
-        FetchAuditError, FetchError, FlockError, JsonParseError, LoadJsonError, LoadTomlError,
-        StoreAcquireError, StoreCommitError, StoreCreateError, StoreJsonError, StoreTomlError,
-        StoreValidateErrors, TomlParseError, UnpackError,
+        FetchAuditError, FetchError, FlockError, InvalidCriteriaError, JsonParseError,
+        LoadJsonError, LoadTomlError, SourceFile, StoreAcquireError, StoreCommitError,
+        StoreCreateError, StoreJsonError, StoreTomlError, StoreValidateError, StoreValidateErrors,
+        TomlParseError, UnpackError,
     },
     flock::{FileLock, Filesystem},
     format::{
-        AuditsFile, CommandHistory, ConfigFile, Delta, DiffCache, DiffStat, FastMap, FetchCommand,
-        ImportsFile, MetaConfig, PackageStr, SortedMap,
+        AuditsFile, CommandHistory, ConfigFile, CriteriaName, Delta, DiffCache, DiffStat, FastMap,
+        FetchCommand, ImportsFile, MetaConfig, PackageStr, SortedMap, SAFE_TO_DEPLOY, SAFE_TO_RUN,
     },
     network::Network,
     resolver,
-    serialization::to_formatted_toml,
+    serialization::{spanned::Spanned, to_formatted_toml},
     Config, PartialConfig,
 };
 
@@ -120,6 +122,10 @@ pub struct Store {
     pub config: ConfigFile,
     pub imports: ImportsFile,
     pub audits: AuditsFile,
+
+    pub config_src: SourceFile,
+    pub imports_src: SourceFile,
+    pub audits_src: SourceFile,
 }
 
 impl Store {
@@ -145,6 +151,9 @@ impl Store {
                 criteria: SortedMap::new(),
                 audits: SortedMap::new(),
             },
+            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
+            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
+            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
         })
     }
 
@@ -163,15 +172,19 @@ impl Store {
         // exclusive one isn't needed.
         let lock = StoreLock::new(&root)?;
 
-        let config: ConfigFile = load_toml(lock.read_config()?)?;
-        let audits: AuditsFile = load_toml(lock.read_audits()?)?;
-        let imports: ImportsFile = load_toml(lock.read_imports()?)?;
+        let (config_src, config): (_, ConfigFile) = load_toml(CONFIG_TOML, lock.read_config()?)?;
+        let (audits_src, audits): (_, AuditsFile) = load_toml(AUDITS_TOML, lock.read_audits()?)?;
+        let (imports_src, imports): (_, ImportsFile) =
+            load_toml(IMPORTS_LOCK, lock.read_imports()?)?;
 
         let store = Self {
             lock: Some(lock),
             config,
             audits,
             imports,
+            config_src,
+            audits_src,
+            imports_src,
         };
 
         // Check that the store isn't corrupt
@@ -188,9 +201,37 @@ impl Store {
             config,
             imports,
             audits,
+            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
+            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
+            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
         }
     }
 
+    #[cfg(test)]
+    pub fn mock_acquire(
+        config: &str,
+        audits: &str,
+        imports: &str,
+    ) -> Result<Self, StoreAcquireError> {
+        let (config_src, config): (_, ConfigFile) = load_toml(CONFIG_TOML, config.as_bytes())?;
+        let (audits_src, audits): (_, AuditsFile) = load_toml(AUDITS_TOML, audits.as_bytes())?;
+        let (imports_src, imports): (_, ImportsFile) = load_toml(IMPORTS_LOCK, imports.as_bytes())?;
+
+        let store = Self {
+            lock: None,
+            config,
+            imports,
+            audits,
+            config_src,
+            audits_src,
+            imports_src,
+        };
+
+        store.validate()?;
+
+        Ok(store)
+    }
+
     /// Create a clone of the store for use to resolve `suggest`.
     ///
     /// This cloned store will not contain `unaudited` entries from the config,
@@ -206,6 +247,9 @@ impl Store {
             config: self.config.clone(),
             imports: self.imports.clone(),
             audits: self.audits.clone(),
+            config_src: self.config_src.clone(),
+            audits_src: self.audits_src.clone(),
+            imports_src: self.imports_src.clone(),
         };
         // Delete all unaudited entries except those that are suggest=false
         for versions in &mut clone.config.unaudited.values_mut() {
@@ -230,26 +274,155 @@ impl Store {
     }
 
     /// Validate the store's integrity
+    #[allow(clippy::for_kv_map)]
     pub fn validate(&self) -> Result<(), StoreValidateErrors> {
         // ERRORS: ideally these are all gathered diagnostics, want to report as many errors
         // at once as possible!
 
         // TODO(#66): implement validation
         //
-        // * check that policy entries are only first-party
+        // * check that policy entries are only first-party?
+        //   * (we currently allow policy.criteria on third-parties for audit-as-crates-io)
         // * check that unaudited entries are for things that exist?
         // * check that lockfile and imports aren't desync'd (catch new/removed import urls)
         //
         // * check that each CriteriaEntry has 'description' or 'description_url'
         // * check that no one is trying to shadow builtin criteria (safe-to-run, safe-to-deploy)
-        // * check that all criteria are valid in:
-        //   * CriteriaEntry::implies
-        //   * AuditEntry::criteria
-        //   * DependencyCriteria
         // * check that all 'audits' entries are well-formed
         // * check that all package names are valid (with crates.io...?)
         // * check that all reviews have a 'who' (currently an Option to stub it out)
         // * catch no-op deltas?
+        // * nested check imports, complicated because of namespaces
+
+        fn check_criteria(
+            source_code: &SourceFile,
+            valid: &Arc<Vec<CriteriaName>>,
+            errors: &mut Vec<InvalidCriteriaError>,
+            criteria: &[Spanned<CriteriaName>],
+        ) {
+            for criteria in criteria {
+                if !valid.contains(criteria) {
+                    errors.push(InvalidCriteriaError {
+                        source_code: source_code.clone(),
+                        span: Spanned::span(criteria),
+                        invalid: criteria.to_string(),
+                        valid_names: valid.clone(),
+                    })
+                }
+            }
+        }
+
+        // Fixme: this should probably be a Map...? Sorted? Stable?
+        let valid_criteria = Arc::new(
+            self.audits
+                .criteria
+                .iter()
+                .map(|(c, _)| &**c)
+                .chain([SAFE_TO_RUN, SAFE_TO_DEPLOY])
+                .map(|name| name.to_string())
+                .collect::<Vec<_>>(),
+        );
+        let no_criteria = vec![];
+        let mut invalid_criteria_errors = vec![];
+
+        for (_package, entries) in &self.config.unaudited {
+            for entry in entries {
+                check_criteria(
+                    &self.config_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    &entry.criteria,
+                );
+                for (_dep_package, dep_criteria) in &entry.dependency_criteria {
+                    check_criteria(
+                        &self.config_src,
+                        &valid_criteria,
+                        &mut invalid_criteria_errors,
+                        dep_criteria,
+                    );
+                }
+            }
+        }
+        for (_package, policy) in &self.config.policy {
+            check_criteria(
+                &self.config_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                policy.criteria.as_ref().unwrap_or(&no_criteria),
+            );
+            check_criteria(
+                &self.config_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                policy.dev_criteria.as_ref().unwrap_or(&no_criteria),
+            );
+            for (_dep_package, dep_criteria) in &policy.dependency_criteria {
+                check_criteria(
+                    &self.config_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    dep_criteria,
+                );
+            }
+        }
+        for (_new_criteria, entry) in &self.audits.criteria {
+            // TODO: check that new_criteria isn't shadowing a builtin criteria
+            check_criteria(
+                &self.audits_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                &entry.implies,
+            );
+        }
+        for (_package, entries) in &self.audits.audits {
+            for entry in entries {
+                // TODO: check that new_criteria isn't shadowing a builtin criteria
+                check_criteria(
+                    &self.audits_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    &entry.criteria,
+                );
+                match &entry.kind {
+                    crate::format::AuditKind::Full {
+                        dependency_criteria,
+                        ..
+                    } => {
+                        for (_dep_package, dep_criteria) in dependency_criteria {
+                            check_criteria(
+                                &self.audits_src,
+                                &valid_criteria,
+                                &mut invalid_criteria_errors,
+                                dep_criteria,
+                            );
+                        }
+                    }
+                    crate::format::AuditKind::Delta {
+                        dependency_criteria,
+                        ..
+                    } => {
+                        for (_dep_package, dep_criteria) in dependency_criteria {
+                            check_criteria(
+                                &self.audits_src,
+                                &valid_criteria,
+                                &mut invalid_criteria_errors,
+                                dep_criteria,
+                            );
+                        }
+                    }
+                    crate::format::AuditKind::Violation { .. } => {}
+                }
+            }
+        }
+
+        let errors = invalid_criteria_errors
+            .into_iter()
+            .map(StoreValidateError::InvalidCriteria)
+            .collect::<Vec<_>>();
+        if !errors.is_empty() {
+            return Err(StoreValidateErrors { errors });
+        }
+
         Ok(())
     }
 
@@ -287,8 +460,18 @@ async fn fetch_foreign_audit(
         error,
     })?;
     let audit_bytes = network.download(parsed_url).await?;
-    let audit_file: AuditsFile =
-        toml_edit::de::from_slice(&audit_bytes).map_err(|error| TomlParseError { error })?;
+    let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
+    let audit_source = Arc::new(NamedSource::new(name, audit_string.clone()));
+    let audit_file: AuditsFile = toml::de::from_str(&audit_string)
+        .map_err(|error| {
+            let (line, col) = error.line_col().unwrap_or((0, 0));
+            TomlParseError {
+                source_code: audit_source,
+                span: SourceOffset::from_location(&audit_string, line + 1, col + 1),
+                error,
+            }
+        })
+        .map_err(LoadTomlError::from)?;
     Ok(audit_file)
 }
 
@@ -435,7 +618,7 @@ impl Cache {
             .unwrap_or_else(|| root.join(CACHE_DIFF_CACHE));
         let diff_cache: DiffCache = File::open(&diff_cache_path)
             .ok()
-            .and_then(|f| load_toml(f).ok())
+            .and_then(|f| load_toml(CACHE_DIFF_CACHE, f).map(|v| v.1).ok())
             .unwrap_or_default();
 
         // Setup the command_history.
@@ -1032,15 +1215,28 @@ fn find_cargo_registry() -> Result<CargoRegistry, crates_index::Error> {
     })
 }
 
-fn load_toml<T>(reader: impl Read) -> Result<T, LoadTomlError>
+fn load_toml<T>(file_name: &str, reader: impl Read) -> Result<(SourceFile, T), LoadTomlError>
 where
     T: for<'a> Deserialize<'a>,
 {
     let mut reader = BufReader::new(reader);
     let mut string = String::new();
     reader.read_to_string(&mut string)?;
-    let toml = toml_edit::de::from_str(&string).map_err(|error| TomlParseError { error })?;
-    Ok(toml)
+    let result = toml::de::from_str(&string);
+    // We defer the NamedSource creation into each branch because the error path wants to
+    // have access to the input string, but NamedSource consumes and erases it.
+    match result {
+        Ok(toml) => Ok((Arc::new(NamedSource::new(file_name, string)), toml)),
+        Err(error) => {
+            let (line, col) = error.line_col().unwrap_or((0, 0));
+            let span = SourceOffset::from_location(&string, line + 1, col);
+            Err(TomlParseError {
+                source_code: Arc::new(NamedSource::new(file_name, string)),
+                span,
+                error,
+            })?
+        }
+    }
 }
 fn store_toml<T>(mut writer: impl Write, heading: &str, val: T) -> Result<(), StoreTomlError>
 where


Test patch:
diff --git a/src/tests/audit_as_crates_io.rs b/src/tests/audit_as_crates_io.rs
index a8382e4..827d5b6 100644
--- a/src/tests/audit_as_crates_io.rs
+++ b/src/tests/audit_as_crates_io.rs
@@ -8,6 +8,19 @@ fn get_audit_as_crates_io(cfg: &Config, store: &Store) -> String {
     }
 }
 
+fn get_audit_as_crates_io_json(cfg: &Config, store: &Store) -> String {
+    let res = crate::check_audit_as_crates_io(cfg, store);
+    match res {
+        Ok(()) => String::new(),
+        Err(e) => {
+            let handler = miette::JSONReportHandler::new();
+            let mut output = String::new();
+            handler.render_report(&mut output, &e).unwrap();
+            output
+        }
+    }
+}
+
 #[test]
 fn simple_audit_as_crates_io() {
     let _enter = TEST_RUNTIME.enter();
@@ -21,3 +34,201 @@ fn simple_audit_as_crates_io() {
     let output = get_audit_as_crates_io(&cfg, &store);
     insta::assert_snapshot!("simple-audit-as-crates-io", output);
 }
+
+#[test]
+fn simple_audit_as_crates_io_all_true() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::simple();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    for package in &mock.packages {
+        config
+            .policy
+            .insert(package.name.to_owned(), audit_as_policy(Some(true)));
+    }
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("simple-audit-as-crates-io-all-true", output);
+}
+
+#[test]
+fn simple_audit_as_crates_io_all_false() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::simple();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    for package in &mock.packages {
+        config
+            .policy
+            .insert(package.name.to_owned(), audit_as_policy(Some(false)));
+    }
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("simple-audit-as-crates-io-all-false", output);
+}
+
+#[test]
+fn complex_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::complex();
+    let metadata = mock.metadata();
+    let (config, audits, imports) = builtin_files_full_audited(&metadata);
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("complex-audit-as-crates-io", output);
+}
+
+#[test]
+fn complex_audit_as_crates_io_all_true() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::complex();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    for package in &mock.packages {
+        config
+            .policy
+            .insert(package.name.to_owned(), audit_as_policy(Some(true)));
+    }
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("complex-audit-as-crates-io-all-true", output);
+}
+
+#[test]
+fn complex_audit_as_crates_io_all_false() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::complex();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    for package in &mock.packages {
+        config
+            .policy
+            .insert(package.name.to_owned(), audit_as_policy(Some(false)));
+    }
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("complex-audit-as-crates-io-all-false", output);
+}
+
+#[test]
+fn complex_audit_as_crates_io_max_wrong() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::complex();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    config
+        .policy
+        .insert("rootA".to_owned(), audit_as_policy(Some(true)));
+    config
+        .policy
+        .insert("rootB".to_owned(), audit_as_policy(Some(true)));
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("complex-audit-as-crates-io-max-wrong", output);
+}
+
+#[test]
+fn complex_audit_as_crates_io_max_wrong_json() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::complex();
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_full_audited(&metadata);
+
+    config
+        .policy
+        .insert("rootA".to_owned(), audit_as_policy(Some(true)));
+    config
+        .policy
+        .insert("rootB".to_owned(), audit_as_policy(Some(true)));
+
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io_json(&cfg, &store);
+    insta::assert_snapshot!("complex-audit-as-crates-io-max-wrong-json", output);
+}
+
+#[test]
+fn simple_deps_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::simple_deps();
+    let metadata = mock.metadata();
+    let (config, audits, imports) = builtin_files_full_audited(&metadata);
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("simple-deps-audit-as-crates-io", output);
+}
+
+#[test]
+fn dev_detection_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::dev_detection();
+    let metadata = mock.metadata();
+    let (config, audits, imports) = builtin_files_full_audited(&metadata);
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("dev-detection-audit-as-crates-io", output);
+}
+
+#[test]
+fn haunted_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::haunted_tree();
+    let metadata = mock.metadata();
+    let (config, audits, imports) = builtin_files_full_audited(&metadata);
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("haunted-audit-as-crates-io", output);
+}
+
+#[test]
+fn cycle_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
+    let mock = MockMetadata::cycle();
+    let metadata = mock.metadata();
+    let (config, audits, imports) = builtin_files_full_audited(&metadata);
+    let store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+
+    let output = get_audit_as_crates_io(&cfg, &store);
+    insta::assert_snapshot!("cycle-audit-as-crates-io", output);
+}
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 2453e0e..63830c8 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -26,6 +26,7 @@ use crate::{
 mod audit_as_crates_io;
 mod certify;
 mod regenerate_unaudited;
+mod store_parsing;
 mod vet;
 mod violations;
 
@@ -106,8 +107,32 @@ impl MockRegistry {
     pub fn testing_cinematic_universe() -> Self {
         Self {
             packages: [
+                ("root-package", vec![reg_ver(DEFAULT_VER)]),
                 ("third-party1", vec![reg_ver(DEFAULT_VER)]),
+                ("third-party2", vec![reg_ver(DEFAULT_VER)]),
+                ("transitive-third-party1", vec![reg_ver(DEFAULT_VER)]),
                 ("first-party", vec![reg_ver(DEFAULT_VER)]),
+                ("firstA", vec![reg_ver(DEFAULT_VER)]),
+                ("firstAB", vec![reg_ver(DEFAULT_VER)]),
+                ("firstB", vec![reg_ver(DEFAULT_VER)]),
+                ("firstB-nodeps", vec![reg_ver(DEFAULT_VER)]),
+                ("thirdA", vec![reg_ver(DEFAULT_VER)]),
+                ("thirdAB", vec![reg_ver(DEFAULT_VER)]),
+                ("third-core", vec![reg_ver(DEFAULT_VER), reg_ver(5)]),
+                ("normal", vec![reg_ver(DEFAULT_VER)]),
+                ("dev", vec![reg_ver(DEFAULT_VER)]),
+                ("build", vec![reg_ver(DEFAULT_VER)]),
+                ("proc-macro", vec![reg_ver(DEFAULT_VER)]),
+                ("dev-proc-macro", vec![reg_ver(DEFAULT_VER)]),
+                ("build-proc-macro", vec![reg_ver(DEFAULT_VER)]),
+                ("dev-cycle", vec![reg_ver(DEFAULT_VER)]),
+                ("both", vec![reg_ver(DEFAULT_VER)]),
+                ("simple-dev", vec![reg_ver(DEFAULT_VER)]),
+                ("simple-dev-indirect", vec![reg_ver(DEFAULT_VER)]),
+                ("dev-cycle-direct", vec![reg_ver(DEFAULT_VER)]),
+                ("dev-cycle-indirect", vec![reg_ver(DEFAULT_VER)]),
+                ("third-normal", vec![reg_ver(DEFAULT_VER)]),
+                ("third-dev", vec![reg_ver(DEFAULT_VER)]),
             ]
             .into_iter()
             .collect(),
@@ -215,7 +240,7 @@ fn dep_ver(name: &'static str, version: u64) -> MockDependency {
 fn default_unaudited(version: Version, config: &ConfigFile) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![config.default_criteria.clone()],
+        criteria: vec![config.default_criteria.clone().into()],
         dependency_criteria: DependencyCriteria::new(),
         notes: None,
         suggest: true,
@@ -224,7 +249,7 @@ fn default_unaudited(version: Version, config: &ConfigFile) -> UnauditedDependen
 fn unaudited(version: Version, criteria: CriteriaStr) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         dependency_criteria: DependencyCriteria::new(),
         notes: None,
         suggest: true,
@@ -243,7 +268,7 @@ fn unaudited_dep(
 ) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         notes: None,
         suggest: true,
         dependency_criteria: dependency_criteria
@@ -251,7 +276,7 @@ fn unaudited_dep(
             .map(|(k, v)| {
                 (
                     k.into(),
-                    v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                    v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                 )
             })
             .collect(),
@@ -263,7 +288,7 @@ fn delta_audit(from: Version, to: Version, criteria: CriteriaStr) -> AuditEntry
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Delta {
             delta,
             dependency_criteria: DependencyCriteria::default(),
@@ -287,7 +312,7 @@ fn delta_audit_dep(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Delta {
             delta,
             dependency_criteria: dependency_criteria
@@ -295,7 +320,7 @@ fn delta_audit_dep(
                 .map(|(k, v)| {
                     (
                         k.into(),
-                        v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                        v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                     )
                 })
                 .collect(),
@@ -307,7 +332,7 @@ fn full_audit(version: Version, criteria: CriteriaStr) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Full {
             version,
             dependency_criteria: DependencyCriteria::default(),
@@ -322,7 +347,7 @@ fn full_audit_m(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: criteria.into_iter().map(|s| s.into()).collect(),
+        criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
         kind: AuditKind::Full {
             version,
             dependency_criteria: DependencyCriteria::default(),
@@ -343,7 +368,7 @@ fn full_audit_dep(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Full {
             version,
             dependency_criteria: dependency_criteria
@@ -351,7 +376,7 @@ fn full_audit_dep(
                 .map(|(k, v)| {
                     (
                         k.into(),
-                        v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                        v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                     )
                 })
                 .collect(),
@@ -363,7 +388,7 @@ fn violation_hard(version: VersionReq) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![SAFE_TO_RUN.to_string()],
+        criteria: vec![SAFE_TO_RUN.to_string().into()],
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -372,7 +397,7 @@ fn violation(version: VersionReq, criteria: CriteriaStr) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -384,7 +409,7 @@ fn violation_m(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: criteria.into_iter().map(|s| s.into()).collect(),
+        criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -401,9 +426,16 @@ fn default_policy() -> PolicyEntry {
     }
 }
 
+fn audit_as_policy(audit_as_crates_io: Option<bool>) -> PolicyEntry {
+    PolicyEntry {
+        audit_as_crates_io,
+        ..default_policy()
+    }
+}
+
 fn self_policy(criteria: impl IntoIterator<Item = impl Into<CriteriaName>>) -> PolicyEntry {
     PolicyEntry {
-        criteria: Some(criteria.into_iter().map(|s| s.into()).collect()),
+        criteria: Some(criteria.into_iter().map(|s| s.into().into()).collect()),
         ..default_policy()
     }
 }
@@ -422,7 +454,7 @@ fn dep_policy(
             .map(|(k, v)| {
                 (
                     k.into(),
-                    v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                    v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                 )
             })
             .collect(),
@@ -858,7 +890,7 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
         (
             "strong-reviewed".to_string(),
             CriteriaEntry {
-                implies: vec!["reviewed".to_string()],
+                implies: vec!["reviewed".to_string().into()],
                 description: Some("strongly reviewed".to_string()),
                 description_url: None,
             },
@@ -866,7 +898,7 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
         (
             "reviewed".to_string(),
             CriteriaEntry {
-                implies: vec!["weak-reviewed".to_string()],
+                implies: vec!["weak-reviewed".to_string().into()],
                 description: Some("reviewed".to_string()),
                 description_url: None,
             },
@@ -897,8 +929,8 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
                     package.name.clone(),
                     PolicyEntry {
                         audit_as_crates_io: None,
-                        criteria: Some(vec![DEFAULT_CRIT.to_string()]),
-                        dev_criteria: Some(vec![DEFAULT_CRIT.to_string()]),
+                        criteria: Some(vec![DEFAULT_CRIT.to_string().into()]),
+                        dev_criteria: Some(vec![DEFAULT_CRIT.to_string().into()]),
                         dependency_criteria: DependencyCriteria::new(),
                         targets: None,
                         dev_targets: None,
@@ -913,8 +945,8 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
     // Rewrite the default used by init
     for unaudited in &mut config.unaudited {
         for entry in unaudited.1 {
-            assert_eq!(&*entry.criteria, &["safe-to-deploy"]);
-            entry.criteria = vec![DEFAULT_CRIT.to_string()];
+            assert_eq!(&*entry.criteria, &["safe-to-deploy".to_string()]);
+            entry.criteria = vec![DEFAULT_CRIT.to_string().into()];
         }
     }
 
@@ -984,7 +1016,10 @@ fn builtin_files_minimal_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile
             audited
                 .entry(name.clone())
                 .or_insert(vec![])
-                .push(full_audit_m(entry.version, &entry.criteria));
+                .push(full_audit_m(
+                    entry.version,
+                    entry.criteria.iter().map(|s| &**s).collect::<Vec<_>>(),
+                ));
         }
     }
     audits.audits = audited;
@@ -1067,7 +1102,7 @@ impl<'a> Out for BasicTestOutput<'a> {
         Ok(())
     }
 
-    fn read_line_initial_text(&mut self, initial: &str) -> io::Result<String> {
+    fn read_line_with_prompt(&mut self, initial: &str) -> io::Result<String> {
         write!(self, "{}", initial)?;
         if let Some(on_read_line) = &mut self.on_read_line {
             let response = on_read_line(initial)?;
diff --git a/src/tests/regenerate_unaudited.rs b/src/tests/regenerate_unaudited.rs
index 37044ee..d1c3c60 100644
--- a/src/tests/regenerate_unaudited.rs
+++ b/src/tests/regenerate_unaudited.rs
@@ -434,3 +434,59 @@ fn builtin_simple_unaudited_nested_stronger_req_regenerate() {
         unaudited
     );
 }
+
+#[test]
+fn builtin_simple_audit_as_default_root_regenerate() {
+    // (Pass) the root is audit-as-crates-io with a default root policy
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config
+        .policy
+        .insert("root-package".to_string(), audit_as_policy(Some(true)));
+    config.unaudited.insert(
+        "root-package".to_string(),
+        vec![unaudited(ver(DEFAULT_VER), SAFE_TO_DEPLOY)],
+    );
+
+    let mut store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+    crate::minimize_unaudited(&cfg, &mut store, None).unwrap();
+
+    let unaudited = get_unaudited(&store);
+    insta::assert_snapshot!("builtin-simple-audit-as-default-root-regenerate", unaudited);
+}
+
+#[test]
+fn builtin_simple_audit_as_weaker_root_regenerate() {
+    // (Pass) the root is audit-as-crates-io with an explicit root policy
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config.policy.insert(
+        "root-package".to_string(),
+        PolicyEntry {
+            criteria: Some(vec![SAFE_TO_RUN.to_string().into()]),
+            ..audit_as_policy(Some(true))
+        },
+    );
+    config.unaudited.insert(
+        "root-package".to_string(),
+        vec![unaudited(ver(DEFAULT_VER), SAFE_TO_DEPLOY)],
+    );
+
+    let mut store = Store::mock(config, audits, imports);
+    let cfg = mock_cfg(&metadata);
+    crate::minimize_unaudited(&cfg, &mut store, None).unwrap();
+
+    let unaudited = get_unaudited(&store);
+    insta::assert_snapshot!("builtin-simple-audit-as-weaker-root-regenerate", unaudited);
+}
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-false.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-false.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-false.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-true.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-true.snap
new file mode 100644
index 0000000..c0d10f7
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-all-true.snap
@@ -0,0 +1,13 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
+  × There are some issues with your policy.audit-as-crates-io entries
+
+Error: 
+  × some audit-as-crates-io packages don't match published crates.io versions
+  │   rootA:10.0.0
+  │   rootB:10.0.0
+  help: Remove the audit-as-crates-io entries or make them `false`
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap
new file mode 100644
index 0000000..dcfcb24
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+{"message": "There are some issues with your policy.audit-as-crates-io entries","severity": "error","labels": [],"related": [{"message": "Some non-crates.io-fetched packages match published crates.io versions\n  firstA:10.0.0\n  firstAB:10.0.0\n  firstB:10.0.0\n  firstB-nodeps:10.0.0","severity": "error","help": "Add a `policy.*.audit-as-crates-io` entry for them","labels": [],"related": []},{"message": "some audit-as-crates-io packages don't match published crates.io versions\n  rootA:10.0.0\n  rootB:10.0.0","severity": "error","help": "Remove the audit-as-crates-io entries or make them `false`","labels": [],"related": []}]}
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong.snap
new file mode 100644
index 0000000..d6d1040
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong.snap
@@ -0,0 +1,20 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
+  × There are some issues with your policy.audit-as-crates-io entries
+
+Error: 
+  × Some non-crates.io-fetched packages match published crates.io versions
+  │   firstA:10.0.0
+  │   firstAB:10.0.0
+  │   firstB:10.0.0
+  │   firstB-nodeps:10.0.0
+  help: Add a `policy.*.audit-as-crates-io` entry for them
+Error: 
+  × some audit-as-crates-io packages don't match published crates.io versions
+  │   rootA:10.0.0
+  │   rootB:10.0.0
+  help: Remove the audit-as-crates-io entries or make them `false`
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io.snap
new file mode 100644
index 0000000..24aa646
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io.snap
@@ -0,0 +1,15 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
+  × There are some issues with your policy.audit-as-crates-io entries
+
+Error: 
+  × Some non-crates.io-fetched packages match published crates.io versions
+  │   firstA:10.0.0
+  │   firstAB:10.0.0
+  │   firstB:10.0.0
+  │   firstB-nodeps:10.0.0
+  help: Add a `policy.*.audit-as-crates-io` entry for them
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__cycle-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__cycle-audit-as-crates-io.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__cycle-audit-as-crates-io.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__dev-detection-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__dev-detection-audit-as-crates-io.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__dev-detection-audit-as-crates-io.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__haunted-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__haunted-audit-as-crates-io.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__haunted-audit-as-crates-io.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-false.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-false.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-false.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-true.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-true.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io-all-true.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
index 0662b49..3ca89fe 100644
--- a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
@@ -6,7 +6,8 @@ expression: output
   × There are some issues with your policy.audit-as-crates-io entries
 
 Error: 
-  × Some first-party packages match published crates.io versions
+  × Some non-crates.io-fetched packages match published crates.io versions
+  │   root-package:10.0.0
   │   first-party:10.0.0
   help: Add a `policy.*.audit-as-crates-io` entry for them
 
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-deps-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-deps-audit-as-crates-io.snap
new file mode 100644
index 0000000..14b0b25
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-deps-audit-as-crates-io.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/audit_as_crates_io.rs
+expression: output
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-default-root-regenerate.snap b/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-default-root-regenerate.snap
new file mode 100644
index 0000000..ffac36b
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-default-root-regenerate.snap
@@ -0,0 +1,20 @@
+---
+source: src/tests/regenerate_unaudited.rs
+expression: unaudited
+---
+[[root-package]]
+version = "10.0.0"
+criteria = "safe-to-deploy"
+
+[[third-party1]]
+version = "10.0.0"
+criteria = "safe-to-deploy"
+
+[[third-party2]]
+version = "10.0.0"
+criteria = "safe-to-deploy"
+
+[[transitive-third-party1]]
+version = "10.0.0"
+criteria = "safe-to-deploy"
+
diff --git a/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-weaker-root-regenerate.snap b/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-weaker-root-regenerate.snap
new file mode 100644
index 0000000..f2e8775
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__regenerate_unaudited__builtin-simple-audit-as-weaker-root-regenerate.snap
@@ -0,0 +1,20 @@
+---
+source: src/tests/regenerate_unaudited.rs
+expression: unaudited
+---
+[[root-package]]
+version = "10.0.0"
+criteria = "safe-to-run"
+
+[[third-party1]]
+version = "10.0.0"
+criteria = "safe-to-run"
+
+[[third-party2]]
+version = "10.0.0"
+criteria = "safe-to-run"
+
+[[transitive-third-party1]]
+version = "10.0.0"
+criteria = "safe-to-run"
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap
new file mode 100644
index 0000000..6f0b2b0
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap
@@ -0,0 +1,13 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Failed to parse toml file
+  ╰─▶ missing field `audits`
+   ╭─[audits.toml:1:1]
+ 1 │ 
+   · ▲
+   · ╰── here
+   ╰────
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap
new file mode 100644
index 0000000..802d949
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
new file mode 100644
index 0000000..9f3eb7a
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
@@ -0,0 +1,106 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad-imply' is not a valid criteria name
+   ╭─[audits.toml:3:1]
+ 3 │ description = "great"
+ 4 │ implies = ["safe-to-deploy", "bad-imply"]
+   ·                              ───────────
+ 5 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[audits.toml:7:1]
+ 7 │ version = "1.0.0"
+ 8 │ criteria = "bad"
+   ·            ─────
+ 9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad1' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                                           ──────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad2' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                                                           ──────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad-dep' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                ─────────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'dang' is not a valid criteria name
+    ╭─[audits.toml:12:1]
+ 12 │ delta = "1.0.0 -> 1.1.0"
+ 13 │ criteria = ["safe-to-run", "dang"]
+    ·                            ──────
+ 14 │ dependency-criteria = {}
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'oops' is not a valid criteria name
+    ╭─[audits.toml:17:1]
+ 17 │ delta = "1.0.0 -> 1.1.0"
+ 18 │ criteria = "oops"
+    ·            ──────
+ 19 │ dependency-criteria = { "nope" = "nah" }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nah' is not a valid criteria name
+    ╭─[audits.toml:18:1]
+ 18 │ criteria = "oops"
+ 19 │ dependency-criteria = { "nope" = "nah" }
+    ·                                  ─────
+ 20 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'safe-to-jog' is not a valid criteria name
+    ╭─[audits.toml:22:1]
+ 22 │ version = "2.0.0"
+ 23 │ criteria = "safe-to-jog"
+    ·            ─────────────
+ 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-to-destroy' is not a valid criteria name
+    ╭─[audits.toml:23:1]
+ 23 │ criteria = "safe-to-jog"
+ 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
+    ·                                ─────────────────────
+ 25 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'no-good-bad-bad' is not a valid criteria name
+    ╭─[audits.toml:27:1]
+ 27 │ violation = "5.0.0 "
+ 28 │ criteria = "no-good-bad-bad"
+    ·            ─────────────────
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
new file mode 100644
index 0000000..96d018f
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
@@ -0,0 +1,71 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'oops' is not a valid criteria name
+    ╭─[config.toml:16:1]
+ 16 │ version = "1.0.0"
+ 17 │ criteria = "oops"
+    ·            ──────
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nah' is not a valid criteria name
+    ╭─[config.toml:17:1]
+ 17 │ criteria = "oops"
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ·                                       ─────
+ 19 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'no' is not a valid criteria name
+    ╭─[config.toml:17:1]
+ 17 │ criteria = "oops"
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ·                                                      ────
+ 19 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[config.toml:2:1]
+ 2 │ [policy.serde]
+ 3 │ criteria = "bad"
+   ·            ─────
+ 4 │ dev-criteria = "nope"
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nope' is not a valid criteria name
+   ╭─[config.toml:3:1]
+ 3 │ criteria = "bad"
+ 4 │ dev-criteria = "nope"
+   ·                ──────
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-for-all' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                                                       ────────────────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nada' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                        ──────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap
new file mode 100644
index 0000000..77357fe
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap
@@ -0,0 +1,44 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[config.toml:2:1]
+ 2 │ [policy.serde]
+ 3 │ criteria = "bad"
+   ·            ─────
+ 4 │ dev-criteria = "nope"
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nope' is not a valid criteria name
+   ╭─[config.toml:3:1]
+ 3 │ criteria = "bad"
+ 4 │ dev-criteria = "nope"
+   ·                ──────
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-for-all' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                                                       ────────────────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nada' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                        ──────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
new file mode 100644
index 0000000..a3ba998
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
@@ -0,0 +1,16 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[audits.toml:3:1]
+ 3 │ version = "1.0.0"
+ 4 │ criteria = "bad"
+   ·            ─────
+   ╰────
+  help: the possible criteria are ["safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-no-audit.snap b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-no-audit.snap
new file mode 100644
index 0000000..9f89c55
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-no-audit.snap
@@ -0,0 +1,16 @@
+---
+source: src/tests/vet.rs
+expression: output
+---
+Vetting Failed!
+
+1 unvetted dependencies:
+  root-package:10.0.0 missing ["safe-to-deploy"]
+
+recommended audits for safe-to-deploy:
+    cargo vet inspect root-package 10.0.0  (used by )  (100 lines)
+
+estimated audit backlog: 100 lines
+
+Use |cargo vet certify| to record the audits.
+
diff --git a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-too-weak.snap b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-too-weak.snap
new file mode 100644
index 0000000..9f89c55
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root-too-weak.snap
@@ -0,0 +1,16 @@
+---
+source: src/tests/vet.rs
+expression: output
+---
+Vetting Failed!
+
+1 unvetted dependencies:
+  root-package:10.0.0 missing ["safe-to-deploy"]
+
+recommended audits for safe-to-deploy:
+    cargo vet inspect root-package 10.0.0  (used by )  (100 lines)
+
+estimated audit backlog: 100 lines
+
+Use |cargo vet certify| to record the audits.
+
diff --git a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root.snap b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root.snap
new file mode 100644
index 0000000..ccf7683
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-default-root.snap
@@ -0,0 +1,6 @@
+---
+source: src/tests/vet.rs
+expression: output
+---
+Vetting Succeeded (1 fully audited, 3 unaudited)
+
diff --git a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-weaker-root.snap b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-weaker-root.snap
new file mode 100644
index 0000000..ccf7683
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-audit-as-weaker-root.snap
@@ -0,0 +1,6 @@
+---
+source: src/tests/vet.rs
+expression: output
+---
+Vetting Succeeded (1 fully audited, 3 unaudited)
+
diff --git a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-unaudited-in-direct-full.snap b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-unaudited-in-direct-full.snap
index e88c964..f229132 100644
--- a/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-unaudited-in-direct-full.snap
+++ b/src/tests/snapshots/cargo_vet__tests__vet__builtin-simple-unaudited-in-direct-full.snap
@@ -3,6 +3,4 @@ source: src/tests/vet.rs
 expression: output
 ---
 Vetting Succeeded (3 fully audited)
-  warning: some dependencies are listed in unaudited, but didn't need it:
-    third-party1:10.0.0
 
diff --git a/src/tests/store_parsing.rs b/src/tests/store_parsing.rs
new file mode 100644
index 0000000..59515b9
--- /dev/null
+++ b/src/tests/store_parsing.rs
@@ -0,0 +1,110 @@
+const EMPTY_CONFIG: &str = "\n";
+const EMPTY_AUDITS: &str = "[audits]\n";
+const EMPTY_IMPORTS: &str = "[audits]\n";
+
+fn get_valid_store(config: &str, audits: &str, imports: &str) -> String {
+    let res = crate::Store::mock_acquire(config, audits, imports);
+    match res {
+        Ok(_) => String::new(),
+        Err(e) => format!("{:?}", miette::Report::new(e)),
+    }
+}
+
+#[test]
+fn test_all_empty() {
+    let acquire_errors = get_valid_store("\n", "\n", "\n");
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_all_min() {
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, EMPTY_AUDITS, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_simple_bad_audit() {
+    let audits = r##"
+[[audits.serde]]
+version = "1.0.0"
+criteria = "bad"
+"##;
+
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_many_bad_audits() {
+    let audits = r##"
+[criteria.good]
+description = "great"
+implies = ["safe-to-deploy", "bad-imply"]
+
+[[audits.serde]]
+version = "1.0.0"
+criteria = "bad"
+dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+
+[[audits.serde]]
+delta = "1.0.0 -> 1.1.0"
+criteria = ["safe-to-run", "dang"]
+dependency-criteria = {}
+
+[[audits.serde]]
+delta = "1.0.0 -> 1.1.0"
+criteria = "oops"
+dependency-criteria = { "nope" = "nah" }
+
+[[audits.serde]]
+version = "2.0.0"
+criteria = "safe-to-jog"
+dependency-criteria = { toml = ["unsafe-to-destroy"] }
+
+[[audits.serde]]
+violation = "5.0.0 "
+criteria = "no-good-bad-bad"
+"##;
+
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_many_bad_config() {
+    let config = r##"
+[policy.serde]
+criteria = "bad"
+dev-criteria = "nope"
+dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+
+[policy.clap]
+criteria = "safe-to-deploy"
+dev-criteria = "safe-to-run"
+dependency-criteria = { clap_derive = "good" }
+
+[policy.boring]
+audit-as-crates-io = true
+
+[[unaudited.clap]]
+version = "1.0.0"
+criteria = "oops"
+dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+
+[[unaudited.clap_derive]]
+version = "1.0.0"
+criteria = "safe-to-run"
+
+"##;
+
+    let audits = r##"
+[criteria.good]
+description = "great"
+implies = ["safe-to-deploy"]
+
+[audits]
+"##;
+
+    let acquire_errors = get_valid_store(config, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
diff --git a/src/tests/vet.rs b/src/tests/vet.rs
index e9f1b09..cc70443 100644
--- a/src/tests/vet.rs
+++ b/src/tests/vet.rs
@@ -1584,8 +1584,8 @@ fn builtin_dev_detection_empty_deeper() {
 
 #[test]
 fn builtin_simple_unaudited_extra() {
-    // (Warn) there's an extra unused unaudited entry, but the other is needed
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) there's an extra unused unaudited entry, but the other is needed
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -1612,8 +1612,8 @@ fn builtin_simple_unaudited_extra() {
 
 #[test]
 fn builtin_simple_unaudited_not_a_real_dep() {
-    // (Warn) there's an unaudited entry for a package that isn't in our tree at all.
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) there's an unaudited entry for a package that isn't in our tree at all.
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -1635,8 +1635,8 @@ fn builtin_simple_unaudited_not_a_real_dep() {
 
 #[test]
 fn builtin_simple_deps_unaudited_overbroad() {
-    // (Warn) the unaudited entry is needed but it's overbroad
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) the unaudited entry is needed but it's overbroad
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple_deps();
@@ -1714,8 +1714,8 @@ fn builtin_complex_unaudited_partial_twins() {
 
 #[test]
 fn builtin_simple_unaudited_in_delta() {
-    // (Warn) An audited entry overlaps a delta and isn't needed
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) An audited entry overlaps a delta and isn't needed
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -1746,8 +1746,8 @@ fn builtin_simple_unaudited_in_delta() {
 
 #[test]
 fn builtin_simple_unaudited_in_full() {
-    // (Warn) An audited entry overlaps a full audit and isn't needed
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) An audited entry overlaps a full audit and isn't needed
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -1778,7 +1778,8 @@ fn builtin_simple_unaudited_in_full() {
 
 #[test]
 fn builtin_simple_unaudited_in_direct_full() {
-    // (Warn) An audited entry overlaps a full audit which is the cur version and isn't needed
+    // (Pass) An audited entry overlaps a full audit which is the cur version and isn't needed
+    // (This test used to warn when we tried to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -1975,8 +1976,8 @@ fn builtin_simple_unaudited_nested_stronger_req() {
 
 #[test]
 fn builtin_simple_deps_unaudited_adds_uneeded_criteria() {
-    // (Warn) An audited entry overlaps a full audit which is the cur version and isn't needed
-    // BUSTED: this test is broken (doesn't emit warning)
+    // (Pass) An audited entry overlaps a full audit which is the cur version and isn't needed
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple_deps();
@@ -2008,9 +2009,8 @@ fn builtin_simple_deps_unaudited_adds_uneeded_criteria() {
 
 #[test]
 fn builtin_dev_detection_unaudited_adds_uneeded_criteria_indirect() {
-    // (Warn) An audited entry overlaps a full audit which is the cur version and isn't needed
-    // BUSTED: this test is broken (doesn't emit warning)
-    // TODO: or is this test wrong? should the delta apply?
+    // (Pass) An audited entry overlaps a full audit which is the cur version and isn't needed
+    // (This test could warn if we try to detect "useless unaudited" eagerly)
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::dev_detection();
@@ -2128,8 +2128,8 @@ fn builtin_simple_delta_cycle() {
 
 #[test]
 fn builtin_simple_noop_delta() {
-    // (Warn) completely pointless noop delta
-    // BUSTED: fails to warn about a 5->5 delta
+    // (Pass) completely pointless noop delta
+    // (This test could warn if we try to detect "useless deltas")
 
     let _enter = TEST_RUNTIME.enter();
     let mock = MockMetadata::simple();
@@ -2389,3 +2389,103 @@ fn builtin_haunted_minimal_audited() {
     let output = get_report(&metadata, report);
     insta::assert_snapshot!("builtin-haunted-minimal-audited", output);
 }
+
+#[test]
+fn builtin_simple_audit_as_default_root_no_audit() {
+    // (Fail) the root is audit-as-crates-io but has no audits
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config
+        .policy
+        .insert("root-package".to_string(), audit_as_policy(Some(true)));
+
+    let store = Store::mock(config, audits, imports);
+    let report = crate::resolver::resolve(&metadata, None, &store, ResolveDepth::Shallow);
+
+    let output = get_report(&metadata, report);
+    insta::assert_snapshot!("builtin-simple-audit-as-default-root-no-audit", output);
+}
+
+#[test]
+fn builtin_simple_audit_as_default_root() {
+    // (Pass) the root is audit-as-crates-io and only needs to be safe-to-run
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config
+        .policy
+        .insert("root-package".to_string(), audit_as_policy(Some(true)));
+    config.unaudited.insert(
+        "root-package".to_string(),
+        vec![unaudited(ver(DEFAULT_VER), SAFE_TO_DEPLOY)],
+    );
+
+    let store = Store::mock(config, audits, imports);
+    let report = crate::resolver::resolve(&metadata, None, &store, ResolveDepth::Shallow);
+
+    let output = get_report(&metadata, report);
+    insta::assert_snapshot!("builtin-simple-audit-as-default-root", output);
+}
+
+#[test]
+fn builtin_simple_audit_as_default_root_too_weak() {
+    // (Fail) the root is audit-as-crates-io but is only safe-to-run
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config
+        .policy
+        .insert("root-package".to_string(), audit_as_policy(Some(true)));
+    config.unaudited.insert(
+        "root-package".to_string(),
+        vec![unaudited(ver(DEFAULT_VER), SAFE_TO_RUN)],
+    );
+
+    let store = Store::mock(config, audits, imports);
+    let report = crate::resolver::resolve(&metadata, None, &store, ResolveDepth::Shallow);
+
+    let output = get_report(&metadata, report);
+    insta::assert_snapshot!("builtin-simple-audit-as-default-root-too-weak", output);
+}
+
+#[test]
+fn builtin_simple_audit_as_weaker_root() {
+    // (Pass) the root is audit-as-crates-io and only needs to be safe-to-run
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, imports) = builtin_files_inited(&metadata);
+
+    config.policy.insert(
+        "root-package".to_string(),
+        PolicyEntry {
+            criteria: Some(vec![SAFE_TO_RUN.to_string().into()]),
+            ..audit_as_policy(Some(true))
+        },
+    );
+    config.unaudited.insert(
+        "root-package".to_string(),
+        vec![unaudited(ver(DEFAULT_VER), SAFE_TO_RUN)],
+    );
+
+    let store = Store::mock(config, audits, imports);
+    let report = crate::resolver::resolve(&metadata, None, &store, ResolveDepth::Shallow);
+
+    let output = get_report(&metadata, report);
+    insta::assert_snapshot!("builtin-simple-audit-as-weaker-root", output);
+}
diff --git a/tests/snapshots/test_cli__test-project-bad-certify-human.snap b/tests/snapshots/test_cli__test-project-bad-certify-human.snap
new file mode 100644
index 0000000..784f9b8
--- /dev/null
+++ b/tests/snapshots/test_cli__test-project-bad-certify-human.snap
@@ -0,0 +1,10 @@
+---
+source: tests/test-cli.rs
+expression: format_outputs(&output)
+---
+stdout:
+
+stderr:
+
+  × 'asdfsdfs' isn't one of your foreign packages
+
diff --git a/tests/snapshots/test_cli__test-project-bad-certify-json.snap b/tests/snapshots/test_cli__test-project-bad-certify-json.snap
new file mode 100644
index 0000000..c2c97de
--- /dev/null
+++ b/tests/snapshots/test_cli__test-project-bad-certify-json.snap
@@ -0,0 +1,8 @@
+---
+source: tests/test-cli.rs
+expression: format_outputs(&output)
+---
+stdout:
+
+stderr:
+{"message": "'asdfsdfs' isn't one of your foreign packages","severity": "error","labels": [],"related": []}
diff --git a/tests/snapshots/test_cli__test-project-json.snap b/tests/snapshots/test_cli__test-project-json.snap
index 94d1b71..c113983 100644
--- a/tests/snapshots/test_cli__test-project-json.snap
+++ b/tests/snapshots/test_cli__test-project-json.snap
@@ -5,7 +5,6 @@ expression: format_outputs(&output)
 stdout:
 {
   "conclusion": "success",
-  "useless_unaudited": [],
   "vetted_fully": [
     {
       "name": "atty",
diff --git a/tests/test-cli.rs b/tests/test-cli.rs
index cdca482..70e4f95 100644
--- a/tests/test-cli.rs
+++ b/tests/test-cli.rs
@@ -32,7 +32,8 @@ fn format_outputs(output: &Output) -> String {
         .unwrap()
         .lines()
         .filter(|line| !line.starts_with("Blocking: waiting for file lock"))
-        .collect::<String>();
+        .collect::<Vec<_>>()
+        .join("\n");
     format!("stdout:\n{stdout}\nstderr:\n{stderr}")
 }
 
@@ -296,3 +297,48 @@ fn test_project_dump_graph_full() {
     insta::assert_snapshot!("test-project-dump-graph-full", format_outputs(&output));
     assert!(output.status.success(), "{}", output.status);
 }
+
+#[test]
+fn test_project_bad_certify_human() {
+    let project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .join("tests")
+        .join("test-project");
+    let bin = env!("CARGO_BIN_EXE_cargo-vet");
+    let output = Command::new(bin)
+        .current_dir(&project)
+        .arg("vet")
+        .arg("--manifest-path")
+        .arg("Cargo.toml")
+        .arg("certify")
+        .arg("asdfsdfs")
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    insta::assert_snapshot!("test-project-bad-certify-human", format_outputs(&output));
+    assert!(!output.status.success(), "{}", output.status);
+}
+
+#[test]
+fn test_project_bad_certify_json() {
+    let project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .join("tests")
+        .join("test-project");
+    let bin = env!("CARGO_BIN_EXE_cargo-vet");
+    let output = Command::new(bin)
+        .current_dir(&project)
+        .arg("vet")
+        .arg("--output-format=json")
+        .arg("--manifest-path")
+        .arg("Cargo.toml")
+        .arg("certify")
+        .arg("asdfsdfs")
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    insta::assert_snapshot!("test-project-bad-certify-json", format_outputs(&output));
+    assert!(!output.status.success(), "{}", output.status);
+}