Repository: mozilla/cargo-vet (rust)

Issue:
Mechanism for automatically pruning `importable = false` audits
In #520, we added a new option to audits, which allows them to be marked as non-importable. This new flag is specified by default for newly created audits mentioning git revisions, as they have limited value for others consuming your audit list. 

These git audits can in some cases produce a large amount of non-useful audits in the audits file, which would be nice to simplify in an automated fashion.

In order to clean these non-importable audits up automatically, we should introduce automatic pruning, similar to the pruning we do for exemptions, but for non-importable audits. They should probably be preferred in a [similar manner to exemptions](https://github.com/mozilla/cargo-vet/blob/ffcb37a2dd5e026a0f11b049d773305c9ddc73c7/src/resolver.rs#L1439-L1445), but at a higher priority to exemptions (as a non-importable audit is preferred to an exemption). This will handle removing audits in the case where a non-importable audit is useless.

This won't automatically handle removing large audit chains, however, as if we build a chain of non-importable audits it will not be collapsed. To handle this, we can introduce a (perhaps opt-in?) mechanism during `vet certify` which will try to help with this. 

My current (very) rough idea is that before [certifying a non-importable delta audit](https://github.com/mozilla/cargo-vet/blob/ffcb37a2dd5e026a0f11b049d773305c9ddc73c7/src/main.rs#L938-L952), we first check if there is exactly one local non-importable audit with at least the same criteria as our new audit, which has our version as its "to", and where the "from" version is valid for the certified criteria. If this is the case, we'd change the new audit's "from" version to that audit's "from" version - effectively bypassing it, and allowing the pruner to remove the other audit now that it is unused.

We could try to handle more cases, and do this recursively, however that's probably unnecessary.

I imagine there are some edge cases and/or potential simplifications we could do here (e.g. we could remove the "valid for the certified criteria" check if we unconditionally introduce both the original audit and the broader audit, allowing the pruner to clean up after us, but that runs the risk of adding even more junk audits if the pruner fails).

Pull request description:
Collapse non-importable audits when certifying.
Closes #580.

This adds the `--collapse-non-importable` flag (name is up for debate) to the `certify` command.

Patch:
diff --git a/src/cli.rs b/src/cli.rs
index 18811fa..e8f230c 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -543,6 +543,15 @@ pub struct CertifyArgs {
     /// talk about is part of your current build, but this flag disables that.
     #[clap(long, action)]
     pub force: bool,
+    /// Prevent combination of the audit with a prior adjacent non-importable git audit, if any.
+    ///
+    /// This will only have an effect if the supplied `from` version is a git version.
+    ///
+    /// For example, normally an existing audit from `1.0.0->1.0.0@git:1111111` and a new certified
+    /// audit from `1.0.0@git:1111111->1.0.0@git:2222222` would result in a single audit from
+    /// `1.0.0->1.0.0@git:2222222`. Passing this flag would prevent this.
+    #[clap(long, action, requires("version2"))]
+    pub no_collapse: bool,
 }
 
 /// Import a new peer
diff --git a/src/errors.rs b/src/errors.rs
index e046efa..ae5f366 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -296,6 +296,9 @@ pub enum CertifyError {
     #[error("end date of {0} is too far in the future")]
     #[diagnostic(help("wildcard audit end dates may be at most 1 year in the future"))]
     BadWildcardEndDate(chrono::NaiveDate),
+    #[error("couldn't build an audit graph to determine audit collapse validity")]
+    #[diagnostic(help("use --no-collapse to disable audit collapsing"))]
+    BadAuditGraph,
     #[error(transparent)]
     IoError(#[from] std::io::Error),
     #[error(transparent)]
diff --git a/src/format.rs b/src/format.rs
index 2ffc088..40ccb34 100644
--- a/src/format.rs
+++ b/src/format.rs
@@ -303,6 +303,71 @@ impl AuditEntry {
         // semantically and might have been updated uneventfully.
         self.kind == other.kind && self.criteria == other.criteria
     }
+
+    /// Try to collapse this (delta) entry with the given entry, which must be just prior to it
+    /// (whether a delta or a full audit).
+    ///
+    /// If the entry can be collapsed, the new entry is returned. The new entry will merge the
+    /// `who`, `notes`, and `kind` fields appropriately, and will derive `importable` based on
+    /// `kind`.
+    pub fn try_collapse_with_prior(&self, other: &AuditEntry) -> Option<Self> {
+        let AuditKind::Delta {
+            from: self_from,
+            to: self_to,
+        } = &self.kind
+        else {
+            return None;
+        };
+        let (other_from, other_to) = match &other.kind {
+            AuditKind::Full { version } => (None, version),
+            AuditKind::Delta { from, to } => (Some(from), to),
+            AuditKind::Violation { .. } => return None,
+        };
+
+        if other_to != self_from {
+            return None;
+        }
+
+        // TODO should this use a criteria mapper to avoid different orderings?
+        if other.criteria != self.criteria {
+            return None;
+        }
+
+        // Consume the existing audit's `from`, `who`, and `notes`.
+        let mut new_entry = self.clone();
+        new_entry.kind = match other_from {
+            Some(version) => AuditKind::Delta {
+                from: version.clone(),
+                to: self_to.clone(),
+            },
+            None => AuditKind::Full {
+                version: self_to.clone(),
+            },
+        };
+
+        let unique_who: Vec<_> = other
+            .who
+            .iter()
+            .filter(|s| !new_entry.who.contains(s))
+            .cloned()
+            .collect();
+        // Precede with the `who` from the audit which aren't in this
+        // ceritification, to retain a temporal ordering of auditors.
+        new_entry.who.splice(..0, unique_who);
+
+        // Precede the certification notes with the notes from the audit.
+        if let Some(prior_notes) = &other.notes {
+            new_entry.notes = Some(match new_entry.notes {
+                None => prior_notes.clone(),
+                Some(new_notes) => format!("{prior_notes}\n{new_notes}"),
+            });
+        }
+
+        // Rederive `importable` based on the versions.
+        new_entry.importable = new_entry.kind.default_importable();
+
+        Some(new_entry)
+    }
 }
 
 /// Implement PartialOrd manually because the order we want for sorting is
@@ -332,6 +397,16 @@ pub enum AuditKind {
     Violation { violation: VersionReq },
 }
 
+impl AuditKind {
+    pub fn default_importable(&self) -> bool {
+        match self {
+            Self::Full { version } => version.git_rev.is_none(),
+            Self::Delta { from, to } => from.git_rev.is_none() && to.git_rev.is_none(),
+            Self::Violation { .. } => false,
+        }
+    }
+}
+
 /// A "VERSION" or "VERSION -> VERSION"
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct Delta {
diff --git a/src/main.rs b/src/main.rs
index ca4b296..a012ff9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -920,14 +920,15 @@ fn do_cmd_certify(
     let criteria = criteria_names.into_iter().map(|s| s.into()).collect();
     match kind {
         CertifyKind::Full { version } => {
-            let importable = version.git_rev.is_none();
+            let kind = AuditKind::Full { version };
+            let importable = kind.default_importable();
             store
                 .audits
                 .audits
                 .entry(package.clone())
                 .or_default()
                 .push(AuditEntry {
-                    kind: AuditKind::Full { version },
+                    kind,
                     criteria,
                     who,
                     importable,
@@ -937,21 +938,87 @@ fn do_cmd_certify(
                 });
         }
         CertifyKind::Delta { from, to } => {
-            let importable = from.git_rev.is_none() && to.git_rev.is_none();
+            let from_is_git_version = from.git_rev.is_some();
+            let kind = AuditKind::Delta { from, to };
+            let importable = kind.default_importable();
+
+            let mut entry = AuditEntry {
+                kind,
+                criteria,
+                who,
+                importable,
+                notes,
+                aggregated_from: vec![],
+                is_fresh_import: false,
+            };
+
+            // Collapse a delta audit with a git `from` version with a prior audit that is
+            // non-importable and has identical and satisfied criteria.
+            //
+            // We merge an adjacent audit for a prior version with the new audit (updating the new
+            // audit). The later `update_store` call will remove the now-unused prior audit.
+            if from_is_git_version && !sub_args.no_collapse {
+                // A closure which returns whether the given audit entry satisfies the criteria
+                // being certified.
+                let is_rooted_for_criteria = {
+                    let mapper = CriteriaMapper::new(&store.audits.criteria);
+                    let criteria = mapper.criteria_from_list(&entry.criteria);
+                    // If the audit graph fails to load, we always return `false` and thus don't
+                    // make any changes.
+                    let audit_graph = match resolver::AuditGraph::build(
+                        store, &mapper, &package, None,
+                    ) {
+                        Ok(graph) => Some(graph),
+                        Err(_) => {
+                            warn!(
+                                "failed to build audit graph to determine audit collapse validity, so not collapsing any audits"
+                            );
+                            None
+                        }
+                    };
+
+                    move |audit: &AuditEntry| {
+                        let Some(audit_graph) = &audit_graph else {
+                            return false;
+                        };
+                        let version = match &audit.kind {
+                            AuditKind::Delta { from, .. } => from,
+                            AuditKind::Full { .. } => return true,
+                            AuditKind::Violation { .. } => return false,
+                        };
+
+                        // NOTE we use `criteria` of the certification rather than the target audit
+                        // to check root accessibility, which is okay since later in
+                        // `try_collapse_with_prior` we verify that the criteria of the audit is
+                        // identical to that of the certification.
+                        mapper.minimal_indices(&criteria).all(|idx| {
+                            audit_graph
+                                .search(idx, version, resolver::SearchMode::PreferExemptions)
+                                .is_ok()
+                        })
+                    }
+                };
+                for audit in store
+                    .audits
+                    .audits
+                    .get(&package)
+                    .into_iter()
+                    .flatten()
+                    .filter(|a| !a.importable && is_rooted_for_criteria(a))
+                {
+                    if let Some(new_entry) = entry.try_collapse_with_prior(audit) {
+                        entry = new_entry;
+                        break;
+                    }
+                }
+            }
+
             store
                 .audits
                 .audits
                 .entry(package.clone())
                 .or_default()
-                .push(AuditEntry {
-                    kind: AuditKind::Delta { from, to },
-                    criteria,
-                    who,
-                    importable,
-                    notes,
-                    aggregated_from: vec![],
-                    is_fresh_import: false,
-                });
+                .push(entry);
         }
         CertifyKind::Wildcard {
             user_id,


Test patch:
diff --git a/src/tests/certify.rs b/src/tests/certify.rs
index 9e18572..40e47fe 100644
--- a/src/tests/certify.rs
+++ b/src/tests/certify.rs
@@ -294,7 +294,7 @@ fn mock_prune_non_importable_audit() {
                     "10.0.0@git:00112233445566778899aabbccddeeff00112233"
                         .parse()
                         .unwrap(),
-                    "safe-to-deploy",
+                    "reviewed",
                 );
                 entry.importable = false;
                 entry
@@ -305,7 +305,7 @@ fn mock_prune_non_importable_audit() {
                     "10.0.0@git:ffeeddccbbaa99887766554433221100ffeeddcc"
                         .parse()
                         .unwrap(),
-                    "safe-to-deploy",
+                    "reviewed",
                 );
                 entry.notes = Some("This entry intentionally left importable.".into());
                 entry
@@ -339,7 +339,7 @@ fn mock_prune_non_importable_audit() {
             "--who",
             "testing",
             "--criteria",
-            "safe-to-deploy",
+            "reviewed",
         ],
     );
     let sub_args = if let Some(crate::cli::Commands::Certify(sub_args)) = &cfg.cli.command {
@@ -365,6 +365,247 @@ fn mock_prune_non_importable_audit() {
     insta::assert_snapshot!("mock-prune-non-importable-audit", result);
 }
 
+#[test]
+fn mock_collapse_non_importable_audits() {
+    let mock = MockMetadata::simple_local_git();
+
+    let _enter = TEST_RUNTIME.enter();
+    let metadata = mock.metadata();
+
+    let (mut config, mut audits, imports) = files_inited(&metadata);
+
+    config
+        .policy
+        .insert("third-party1".to_string(), audit_as_policy(Some(true)));
+
+    audits.audits.insert(
+        "third-party1".to_owned(),
+        vec![full_audit(ver(10), "reviewed"), {
+            let mut entry = delta_audit(
+                ver(10),
+                "10.0.0@git:00112233445566778899aabbccddeeff00112244"
+                    .parse()
+                    .unwrap(),
+                "reviewed",
+            );
+            entry.importable = false;
+            entry.notes = Some("Old notes".into());
+            entry.who = vec!["testing".to_owned().into(), "other".to_owned().into()];
+
+            entry
+        }],
+    );
+
+    let mut store = Store::mock(config, audits, imports);
+
+    let output = BasicTestOutput::with_callbacks(
+        |_| Ok("\n".to_owned()),
+        |_| {
+            Ok("\
+            I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.\n\
+            \n\
+            New notes\n\
+            \n".to_owned())
+        },
+    );
+
+    let cfg = mock_cfg_args(
+        &metadata,
+        [
+            "cargo",
+            "vet",
+            "certify",
+            "third-party1",
+            "10.0.0@git:00112233445566778899aabbccddeeff00112244",
+            "10.0.0@git:00112233445566778899aabbccddeeff00112233",
+            "--who",
+            "testing",
+            "--criteria",
+            "reviewed",
+        ],
+    );
+    let sub_args = if let Some(crate::cli::Commands::Certify(sub_args)) = &cfg.cli.command {
+        sub_args
+    } else {
+        unreachable!();
+    };
+
+    crate::do_cmd_certify(
+        &output.clone().as_dyn(),
+        &cfg,
+        sub_args,
+        &mut store,
+        None,
+        None,
+    )
+    .expect("do_cmd_certify failed");
+
+    let audits = crate::serialization::to_formatted_toml(&store.audits, None).unwrap();
+
+    let result = format!("OUTPUT:\n{output}\nAUDITS:\n{audits}");
+
+    insta::assert_snapshot!("mock-collapse-non-importable-audits", result);
+}
+
+#[test]
+fn mock_collapse_with_full_audit() {
+    let mock = MockMetadata::simple();
+
+    let _enter = TEST_RUNTIME.enter();
+    let metadata = mock.metadata();
+
+    let (config, mut audits, imports) = files_inited(&metadata);
+
+    audits.audits.insert(
+        "third-party1".to_owned(),
+        vec![{
+            let mut audit = full_audit(
+                "10.0.0@git:00112233445566778899aabbccddeeff00112233"
+                    .parse()
+                    .unwrap(),
+                "reviewed",
+            );
+            audit.importable = false;
+            audit
+        }],
+    );
+
+    let mut store = Store::mock(config, audits, imports);
+
+    let output = BasicTestOutput::with_callbacks(
+        |_| Ok("\n".to_owned()),
+        |_| {
+            Ok("\
+            I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112233 to 10.0.0 of third-party1 in accordance with the above criteria.\n\
+            \n\
+            New notes\n\
+            \n".to_owned())
+        },
+    );
+
+    let cfg = mock_cfg_args(
+        &metadata,
+        [
+            "cargo",
+            "vet",
+            "certify",
+            "third-party1",
+            "10.0.0@git:00112233445566778899aabbccddeeff00112233",
+            "10.0.0",
+            "--who",
+            "testing",
+            "--criteria",
+            "reviewed",
+        ],
+    );
+    let sub_args = if let Some(crate::cli::Commands::Certify(sub_args)) = &cfg.cli.command {
+        sub_args
+    } else {
+        unreachable!();
+    };
+
+    crate::do_cmd_certify(
+        &output.clone().as_dyn(),
+        &cfg,
+        sub_args,
+        &mut store,
+        None,
+        None,
+    )
+    .expect("do_cmd_certify failed");
+
+    let audits = crate::serialization::to_formatted_toml(&store.audits, None).unwrap();
+
+    let result = format!("OUTPUT:\n{output}\nAUDITS:\n{audits}");
+
+    insta::assert_snapshot!("mock-collapse-with-full-audit", result);
+}
+
+#[test]
+fn mock_dont_collapse_incompatible_criteria_non_importable_audits() {
+    let mock = MockMetadata::simple_local_git();
+
+    let _enter = TEST_RUNTIME.enter();
+    let metadata = mock.metadata();
+
+    let (mut config, mut audits, imports) = files_inited(&metadata);
+
+    config
+        .policy
+        .insert("third-party1".to_string(), audit_as_policy(Some(true)));
+
+    audits.audits.insert(
+        "third-party1".to_owned(),
+        vec![full_audit(ver(10), "reviewed"), {
+            let mut entry = delta_audit(
+                ver(10),
+                "10.0.0@git:00112233445566778899aabbccddeeff00112244"
+                    .parse()
+                    .unwrap(),
+                "reviewed",
+            );
+            entry.importable = false;
+            entry.notes = Some("Old notes".into());
+            entry.who = vec!["testing".to_owned().into(), "other".to_owned().into()];
+
+            entry
+        }],
+    );
+
+    let mut store = Store::mock(config, audits, imports);
+
+    let output = BasicTestOutput::with_callbacks(
+        |_| Ok("\n".to_owned()),
+        |_| {
+            Ok("\
+            I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.\n\
+            \n\
+            New notes\n\
+            \n".to_owned())
+        },
+    );
+
+    let cfg = mock_cfg_args(
+        &metadata,
+        [
+            "cargo",
+            "vet",
+            "certify",
+            "third-party1",
+            "10.0.0@git:00112233445566778899aabbccddeeff00112244",
+            "10.0.0@git:00112233445566778899aabbccddeeff00112233",
+            "--who",
+            "testing",
+            "--criteria",
+            "strong-reviewed",
+        ],
+    );
+    let sub_args = if let Some(crate::cli::Commands::Certify(sub_args)) = &cfg.cli.command {
+        sub_args
+    } else {
+        unreachable!();
+    };
+
+    crate::do_cmd_certify(
+        &output.clone().as_dyn(),
+        &cfg,
+        sub_args,
+        &mut store,
+        None,
+        None,
+    )
+    .expect("do_cmd_certify failed");
+
+    let audits = crate::serialization::to_formatted_toml(&store.audits, None).unwrap();
+
+    let result = format!("OUTPUT:\n{output}\nAUDITS:\n{audits}");
+
+    insta::assert_snapshot!(
+        "mock-dont-collapse-incompatible-criteria-non-importable-audits",
+        result
+    );
+}
+
 #[test]
 fn mock_wildcard_certify_flow() {
     let mock = MockMetadata::simple();
diff --git a/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-non-importable-audits.snap b/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-non-importable-audits.snap
new file mode 100644
index 0000000..cacbdb3
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-non-importable-audits.snap
@@ -0,0 +1,58 @@
+---
+source: src/tests/certify.rs
+expression: result
+---
+OUTPUT:
+<<<EDITING VET_CERTIFY>>>
+# Please read the following criteria and then follow the instructions below:
+
+# === BEGIN CRITERIA "reviewed" ===
+#
+# reviewed
+#
+# === END CRITERIA ===
+#
+# Uncomment the following statement:
+
+# I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.
+
+# Add any notes about your audit below this line:
+
+
+<<<EDIT OK>>>
+I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.
+
+New notes
+
+
+<<<END EDIT>>>
+
+AUDITS:
+
+[criteria.fuzzed]
+description = "fuzzed"
+
+[criteria.reviewed]
+description = "reviewed"
+implies = "weak-reviewed"
+
+[criteria.strong-reviewed]
+description = "strongly reviewed"
+implies = "reviewed"
+
+[criteria.weak-reviewed]
+description = "weakly reviewed"
+
+[[audits.third-party1]]
+criteria = "reviewed"
+version = "10.0.0"
+
+[[audits.third-party1]]
+who = ["other", "testing"]
+criteria = "reviewed"
+delta = "10.0.0 -> 10.0.0@git:00112233445566778899aabbccddeeff00112233"
+importable = false
+notes = """
+Old notes
+New notes"""
+
diff --git a/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-with-full-audit.snap b/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-with-full-audit.snap
new file mode 100644
index 0000000..3da1751
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__certify__mock-collapse-with-full-audit.snap
@@ -0,0 +1,51 @@
+---
+source: src/tests/certify.rs
+expression: result
+---
+OUTPUT:
+<<<EDITING VET_CERTIFY>>>
+# Please read the following criteria and then follow the instructions below:
+
+# === BEGIN CRITERIA "reviewed" ===
+#
+# reviewed
+#
+# === END CRITERIA ===
+#
+# Uncomment the following statement:
+
+# I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112233 to 10.0.0 of third-party1 in accordance with the above criteria.
+
+# Add any notes about your audit below this line:
+
+
+<<<EDIT OK>>>
+I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112233 to 10.0.0 of third-party1 in accordance with the above criteria.
+
+New notes
+
+
+<<<END EDIT>>>
+
+AUDITS:
+
+[criteria.fuzzed]
+description = "fuzzed"
+
+[criteria.reviewed]
+description = "reviewed"
+implies = "weak-reviewed"
+
+[criteria.strong-reviewed]
+description = "strongly reviewed"
+implies = "reviewed"
+
+[criteria.weak-reviewed]
+description = "weakly reviewed"
+
+[[audits.third-party1]]
+who = "testing"
+criteria = "reviewed"
+version = "10.0.0"
+notes = "New notes"
+
diff --git a/src/tests/snapshots/cargo_vet__tests__certify__mock-dont-collapse-incompatible-criteria-non-importable-audits.snap b/src/tests/snapshots/cargo_vet__tests__certify__mock-dont-collapse-incompatible-criteria-non-importable-audits.snap
new file mode 100644
index 0000000..913452f
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__certify__mock-dont-collapse-incompatible-criteria-non-importable-audits.snap
@@ -0,0 +1,63 @@
+---
+source: src/tests/certify.rs
+expression: result
+---
+OUTPUT:
+<<<EDITING VET_CERTIFY>>>
+# Please read the following criteria and then follow the instructions below:
+
+# === BEGIN CRITERIA "strong-reviewed" ===
+#
+# strongly reviewed
+#
+# === END CRITERIA ===
+#
+# Uncomment the following statement:
+
+# I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.
+
+# Add any notes about your audit below this line:
+
+
+<<<EDIT OK>>>
+I, testing, certify that I have audited the changes from version 10.0.0@git:00112233445566778899aabbccddeeff00112244 to 10.0.0@git:00112233445566778899aabbccddeeff00112233 of third-party1 in accordance with the above criteria.
+
+New notes
+
+
+<<<END EDIT>>>
+
+AUDITS:
+
+[criteria.fuzzed]
+description = "fuzzed"
+
+[criteria.reviewed]
+description = "reviewed"
+implies = "weak-reviewed"
+
+[criteria.strong-reviewed]
+description = "strongly reviewed"
+implies = "reviewed"
+
+[criteria.weak-reviewed]
+description = "weakly reviewed"
+
+[[audits.third-party1]]
+criteria = "reviewed"
+version = "10.0.0"
+
+[[audits.third-party1]]
+who = ["testing", "other"]
+criteria = "reviewed"
+delta = "10.0.0 -> 10.0.0@git:00112233445566778899aabbccddeeff00112244"
+importable = false
+notes = "Old notes"
+
+[[audits.third-party1]]
+who = "testing"
+criteria = "strong-reviewed"
+delta = "10.0.0@git:00112233445566778899aabbccddeeff00112244 -> 10.0.0@git:00112233445566778899aabbccddeeff00112233"
+importable = false
+notes = "New notes"
+
diff --git a/src/tests/snapshots/cargo_vet__tests__certify__mock-prune-non-importable-audit.snap b/src/tests/snapshots/cargo_vet__tests__certify__mock-prune-non-importable-audit.snap
index e6e7fb6..726b09c 100644
--- a/src/tests/snapshots/cargo_vet__tests__certify__mock-prune-non-importable-audit.snap
+++ b/src/tests/snapshots/cargo_vet__tests__certify__mock-prune-non-importable-audit.snap
@@ -6,26 +6,9 @@ OUTPUT:
 <<<EDITING VET_CERTIFY>>>
 # Please read the following criteria and then follow the instructions below:
 
-# === BEGIN CRITERIA "safe-to-deploy" ===
+# === BEGIN CRITERIA "reviewed" ===
 #
-# This crate will not introduce a serious security vulnerability to production
-# software exposed to untrusted input.
-#
-# Auditors are not required to perform a full logic review of the entire crate.
-# Rather, they must review enough to fully reason about the behavior of all unsafe
-# blocks and usage of powerful imports. For any reasonable usage of the crate in
-# real-world software, an attacker must not be able to manipulate the runtime
-# behavior of these sections in an exploitable or surprising way.
-#
-# Ideally, all unsafe code is fully sound, and ambient capabilities (e.g.
-# filesystem access) are hardened against manipulation and consistent with the
-# advertised behavior of the crate. However, some discretion is permitted. In such
-# cases, the nature of the discretion should be recorded in the `notes` field of
-# the audit record.
-#
-# For crates which generate deployed code (e.g. build dependencies or procedural
-# macros), reasonable usage of the crate should output code which meets the above
-# criteria.
+# reviewed
 #
 # === END CRITERIA ===
 #
@@ -63,13 +46,13 @@ implies = "reviewed"
 description = "weakly reviewed"
 
 [[audits.third-party1]]
-criteria = "safe-to-deploy"
+criteria = "reviewed"
 delta = "10.0.0 -> 10.0.0@git:ffeeddccbbaa99887766554433221100ffeeddcc"
 notes = "This entry intentionally left importable."
 
 [[audits.third-party1]]
 who = "testing"
-criteria = "safe-to-deploy"
+criteria = "reviewed"
 version = "10.0.0"
 notes = """
 These are testing notes. They contain some
diff --git a/tests/snapshots/test_cli__markdown-help.snap b/tests/snapshots/test_cli__markdown-help.snap
index fa03a42..2528d1d 100644
--- a/tests/snapshots/test_cli__markdown-help.snap
+++ b/tests/snapshots/test_cli__markdown-help.snap
@@ -398,6 +398,15 @@ Force the command to ignore whether the package/version makes sense
 To catch typos/mistakes, we check if the thing you're trying to talk about is part of
 your current build, but this flag disables that.
 
+#### `--no-collapse`
+Prevent combination of the audit with a prior adjacent non-importable git audit, if any.
+
+This will only have an effect if the supplied `from` version is a git version.
+
+For example, normally an existing audit from `1.0.0->1.0.0@git:1111111` and a new
+certified audit from `1.0.0@git:1111111->1.0.0@git:2222222` would result in a single
+audit from `1.0.0->1.0.0@git:2222222`. Passing this flag would prevent this.
+
 #### `-h, --help`
 Print help information