use allow_core::{AllowEntry, Finding, FindingKind, MatchOutcome, MatchStatus};
use crate::worklist;
const EXPLAIN_PROOF_COMMAND_LIMIT: usize = 8;
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct ExplainReferenceAttention {
pub(super) has_broken_evidence: bool,
pub(super) has_weak_evidence: bool,
pub(super) has_evidence_outside_default_inventory: bool,
pub(super) has_broken_link: bool,
pub(super) has_weak_link: bool,
pub(super) has_link_outside_default_inventory: bool,
}
pub(super) fn explain_next_steps(
entry: &AllowEntry,
findings: &[Finding],
outcomes: &[MatchOutcome],
references: ExplainReferenceAttention,
) -> (Vec<String>, Vec<String>) {
let attention = outcomes
.iter()
.filter(|outcome| outcome.status != MatchStatus::Matched)
.collect::<Vec<_>>();
if let Some(outcome) = attention.first() {
let finding = outcome.finding_index.and_then(|index| findings.get(index));
let kind = worklist::work_item_kind(outcome, finding, Some(entry));
return (
worklist::suggested_actions_for_context(&kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(&kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if references.has_evidence_outside_default_inventory {
let finding = findings.first();
let kind = "broken_evidence_link";
return (
vec![
"commit the referenced evidence file if it should support repository policy"
.to_string(),
"or rerun with --include-untracked when intentionally reviewing local receipt artifacts"
.to_string(),
],
untracked_proof_commands(entry, finding, kind),
);
}
if references.has_link_outside_default_inventory {
let finding = findings.first();
let kind = "broken_evidence_link";
return (
vec![
"commit the referenced traceability file if it should support repository policy"
.to_string(),
"or rerun with --include-untracked when intentionally reviewing local traceability files"
.to_string(),
],
untracked_proof_commands(entry, finding, kind),
);
}
if references.has_broken_evidence {
let finding = findings.first();
let kind = "broken_evidence_link";
return (
worklist::suggested_actions(kind)
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if references.has_broken_link {
let finding = findings.first();
let kind = "broken_evidence_link";
return (
worklist::suggested_link_actions_for_context(kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if references.has_weak_evidence {
let finding = findings.first();
let kind = "weak_evidence_reference";
return (
worklist::suggested_actions_for_context(kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if references.has_weak_link {
let finding = findings.first();
let kind = "weak_evidence_reference";
return (
worklist::suggested_link_actions_for_context(kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if entry.classification == "baseline_debt" {
let finding = findings.first();
let kind = "baseline_debt";
return (
worklist::suggested_actions_for_context(kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
if entry.evidence.is_empty() {
let finding = findings.first();
let kind = if entry.kind == FindingKind::Unsafe {
"unsafe_missing_evidence"
} else {
"missing_evidence"
};
return (
worklist::suggested_actions_for_context(kind, finding, Some(entry))
.into_iter()
.take(2)
.collect(),
worklist::proof_commands(kind, finding, Some(entry))
.into_iter()
.take(EXPLAIN_PROOF_COMMAND_LIMIT)
.collect(),
);
}
(Vec::new(), Vec::new())
}
fn untracked_proof_commands(
entry: &AllowEntry,
finding: Option<&Finding>,
kind: &str,
) -> Vec<String> {
let mut commands = vec![
format!("cargo-allow explain {}", entry.id),
format!("cargo-allow explain {} --include-untracked", entry.id),
format!("cargo-allow worklist --allow-id {} --format json", entry.id),
"cargo-allow check --include-untracked --mode no-new".to_string(),
];
for command in worklist::proof_commands(kind, finding, Some(entry)) {
if !commands.iter().any(|existing| existing == &command) {
commands.push(command);
}
}
commands
}
#[cfg(test)]
mod tests {
use super::super::test_support::{test_entry, test_finding};
use super::*;
use allow_core::{FindingKind, MatchOutcome, MatchStatus};
#[test]
fn explain_next_steps_call_presence_observer() {
let mut entry = test_entry("allow-explain-steps", FindingKind::Panic);
entry.evidence = vec!["test:existing-proof".to_string()];
let finding = test_finding(
FindingKind::Panic,
Some("unwrap"),
"src/lib.rs",
"method_call",
);
let outcome = MatchOutcome {
status: MatchStatus::New,
allow_id: None,
finding_index: Some(0),
message: "new finding".to_string(),
score: 90,
};
let (actions, proofs) = explain_next_steps(
&entry,
std::slice::from_ref(&finding),
&[outcome],
ExplainReferenceAttention::default(),
);
assert_eq!(
actions,
vec![
"remove the new source exception if it is accidental".to_string(),
"or add a reviewed allow entry with owner, reason, scope, evidence, and lifecycle"
.to_string(),
]
);
assert!(proofs.contains(&"cargo-allow explain allow-explain-steps".to_string()));
assert_reference_route(
ExplainReferenceAttention {
has_evidence_outside_default_inventory: true,
..ExplainReferenceAttention::default()
},
"commit the referenced evidence file if it should support repository policy",
"cargo-allow explain allow-explain-steps --include-untracked",
);
assert_reference_route(
ExplainReferenceAttention {
has_link_outside_default_inventory: true,
..ExplainReferenceAttention::default()
},
"commit the referenced traceability file if it should support repository policy",
"cargo-allow explain allow-explain-steps --include-untracked",
);
assert_reference_route(
ExplainReferenceAttention {
has_broken_evidence: true,
..ExplainReferenceAttention::default()
},
"restore or commit the referenced local evidence artifact",
"cargo-allow explain allow-explain-steps",
);
assert_reference_route(
ExplainReferenceAttention {
has_broken_link: true,
..ExplainReferenceAttention::default()
},
"restore or commit the referenced local traceability file",
"cargo-allow explain allow-explain-steps",
);
assert_reference_route(
ExplainReferenceAttention {
has_weak_evidence: true,
..ExplainReferenceAttention::default()
},
"replace the weak evidence string with a typed evidence reference",
"cargo-allow explain allow-explain-steps",
);
assert_reference_route(
ExplainReferenceAttention {
has_weak_link: true,
..ExplainReferenceAttention::default()
},
"replace the weak link string with a typed traceability reference",
"cargo-allow explain allow-explain-steps",
);
let mut baseline_entry = test_entry("allow-baseline", FindingKind::Panic);
baseline_entry.classification = "baseline_debt".to_string();
let (actions, proofs) = explain_next_steps(
&baseline_entry,
&[],
&[],
ExplainReferenceAttention::default(),
);
assert_eq!(
actions.first().map(String::as_str),
Some("replace generated baseline debt with a reviewed allow entry")
);
assert!(proofs.contains(&"cargo-allow explain allow-baseline".to_string()));
let missing_evidence_entry = test_entry("allow-missing-evidence", FindingKind::Unsafe);
let (actions, proofs) = explain_next_steps(
&missing_evidence_entry,
&[],
&[],
ExplainReferenceAttention::default(),
);
assert_eq!(
actions.first().map(String::as_str),
Some("add unsafe-review, test, spec, or boundary evidence for the unsafe exception")
);
assert!(proofs.contains(&"cargo-allow explain allow-missing-evidence".to_string()));
}
#[test]
fn untracked_proof_commands_call_presence_observer() {
let entry = test_entry("allow-untracked", FindingKind::NonRustFile);
let finding = test_finding(
FindingKind::NonRustFile,
None,
"tracked.file",
"tracked_file",
);
let commands = untracked_proof_commands(&entry, Some(&finding), "broken_evidence_link");
assert_eq!(
commands[..4],
[
"cargo-allow explain allow-untracked".to_string(),
"cargo-allow explain allow-untracked --include-untracked".to_string(),
"cargo-allow worklist --allow-id allow-untracked --format json".to_string(),
"cargo-allow check --include-untracked --mode no-new".to_string(),
]
);
assert!(
commands
.contains(&"cargo-allow list --allow-id allow-untracked --format json".to_string())
);
assert_eq!(
commands
.iter()
.filter(|command| command.as_str() == "cargo-allow explain allow-untracked")
.count(),
1
);
}
fn assert_reference_route(
references: ExplainReferenceAttention,
expected_action: &str,
expected_proof_command: &str,
) {
let mut entry = test_entry("allow-explain-steps", FindingKind::Panic);
entry.evidence = vec!["test:existing-proof".to_string()];
let finding = test_finding(
FindingKind::Panic,
Some("unwrap"),
"src/lib.rs",
"method_call",
);
let (actions, proofs) = explain_next_steps(&entry, &[finding], &[], references);
assert_eq!(actions.first().map(String::as_str), Some(expected_action));
assert!(proofs.contains(&expected_proof_command.to_string()));
}
}