use anyhow::Result;
use crate::commands::hook::{self, HookState};
use crate::config::Registry;
use crate::constants;
use crate::daemon;
use crate::json;
use crate::output;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Guaranteed,
Safe,
Widened,
Neutral,
}
impl Verdict {
fn mark(self) -> &'static str {
match self {
Verdict::Guaranteed | Verdict::Safe => "+",
Verdict::Widened => "!",
Verdict::Neutral => " ",
}
}
fn key(self) -> &'static str {
match self {
Verdict::Guaranteed => "guaranteed",
Verdict::Safe => "safe",
Verdict::Widened => "widened",
Verdict::Neutral => "neutral",
}
}
}
pub struct TrustRow {
pub key: &'static str,
pub subject: &'static str,
pub state: String,
pub verdict: Verdict,
}
impl TrustRow {
fn new(
key: &'static str,
subject: &'static str,
state: impl Into<String>,
verdict: Verdict,
) -> Self {
Self {
key,
subject,
state: state.into(),
verdict,
}
}
pub fn verdict_key(&self) -> &'static str {
self.verdict.key()
}
}
pub struct TrustReport {
pub guarantees: Vec<TrustRow>,
pub machine: Vec<TrustRow>,
}
impl TrustReport {
pub fn widened(&self) -> Vec<&str> {
self.machine
.iter()
.filter(|r| r.verdict == Verdict::Widened)
.map(|r| r.subject)
.collect()
}
}
pub fn run(json_output: bool) -> Result<()> {
let registry = Registry::load()?;
let report = build(®istry);
if json_output {
return json::emit(&json::trust_document(&report));
}
print_report(&report);
Ok(())
}
pub(crate) fn build(registry: &Registry) -> TrustReport {
TrustReport {
guarantees: guarantees(),
machine: machine_state(registry),
}
}
fn guarantees() -> Vec<TrustRow> {
use Verdict::Guaranteed as G;
vec![
TrustRow::new(
"filesystem_scope",
"Filesystem scope",
"Registered Git repositories only",
G,
),
TrustRow::new(
"lockfile_verification",
"Lockfile verification",
"Required before every delete",
G,
),
TrustRow::new("symlinks", "Symlinks and junctions", "Refused", G),
TrustRow::new(
"nested_repositories",
"Nested repositories",
"Refused — no lockfile rebuilds someone else's history",
G,
),
TrustRow::new(
"build_outputs",
"Build outputs",
"Never deleted — no dist/, no .next/, no .gitignore rules",
G,
),
TrustRow::new(
"deletion_bypass",
"Deletion bypass",
"None — no flag disables a safety check",
G,
),
TrustRow::new(
"state_writes",
"State writes",
"Atomic — temp file, then rename",
G,
),
TrustRow::new("telemetry", "Telemetry", "None — there is no endpoint", G),
TrustRow::new(
"restore",
"Restore",
"`devp restore --last-run` rebuilds the last pass",
G,
),
]
}
fn machine_state(registry: &Registry) -> Vec<TrustRow> {
let s = ®istry.settings;
let mut rows = vec![
TrustRow::new(
"network",
"Network requests",
if s.update_check {
format!(
"Release check against GitHub, every {} days",
s.update_check_interval_days
)
} else {
"None — the release check is off".to_string()
},
Verdict::Safe,
),
TrustRow::new(
"auto_update",
"Auto-update",
if s.auto_update {
"On — dev-prune replaces its own binary"
} else {
"Off — updates only when you run `devp update`"
},
if s.auto_update {
Verdict::Widened
} else {
Verdict::Safe
},
),
TrustRow::new(
"confirmation",
"Confirmation before deleting",
if s.require_confirmation {
"Required, except where you pass `--yes`"
} else {
"Off — `require_confirmation` is false"
},
if s.require_confirmation {
Verdict::Safe
} else {
Verdict::Widened
},
),
TrustRow::new(
"lockfile_rewrite",
"Lockfile rewriting",
if s.allow_manifest_rewrite {
"Allowed — a stale lockfile is regenerated instead of refused"
} else {
"Refused — verification is read-only"
},
if s.allow_manifest_rewrite {
Verdict::Widened
} else {
Verdict::Safe
},
),
TrustRow::new(
"scheduler",
"Background scheduler",
scheduler_state(),
Verdict::Neutral,
),
TrustRow::new("git_hooks", "Git hooks", hook_state(), Verdict::Neutral),
];
let opt_in = opt_in_adapters(registry);
rows.push(TrustRow::new(
"opt_in_adapters",
"Opt-in adapters",
if opt_in.is_empty() {
"None — only dependency directories are deletable".to_string()
} else {
format!("{} — build trees are deletable too", opt_in.join(", "))
},
if opt_in.is_empty() {
Verdict::Safe
} else {
Verdict::Widened
},
));
rows.push(TrustRow::new(
"repositories",
"Registered repositories",
format!(
"{} — nothing outside them is ever read or written",
registry.repositories.len()
),
Verdict::Neutral,
));
rows.push(TrustRow::new(
"idle_window",
"Idle window",
format!(
"{} days of no commits and no file changes ({} for build trees)",
s.idle_days,
s.build_idle_days.max(s.idle_days)
),
Verdict::Neutral,
));
rows.push(TrustRow::new(
"binary",
"Managed binary",
output::clean_path(daemon::get_exe_path()),
Verdict::Neutral,
));
rows
}
fn opt_in_adapters(registry: &Registry) -> Vec<&'static str> {
let s = ®istry.settings;
[
("gradle", s.enable_gradle),
("maven", s.enable_maven),
("swift", s.enable_swift),
]
.into_iter()
.filter_map(|(name, on)| on.then_some(name))
.collect()
}
fn scheduler_state() -> String {
match daemon::daemon_status() {
Ok(daemon::DaemonStatus::Installed) => "Installed — prunes on its own".to_string(),
Ok(daemon::DaemonStatus::NotInstalled) => {
"Not installed — nothing runs unless you run it".to_string()
}
Ok(daemon::DaemonStatus::Unknown(why)) => format!("Unknown ({why})"),
Err(e) => format!("Unknown ({e})"),
}
}
fn hook_state() -> String {
if !hook::git_available() {
return "Not installed — git is not on PATH".to_string();
}
match hook::state() {
Ok(HookState::Active) => "Installed — new repositories register themselves".to_string(),
Ok(HookState::Absent) => {
"Not installed — repositories register only when you say so".to_string()
}
Ok(HookState::Chained { previous, .. }) => {
format!("Installed, chained to `{previous}`")
}
Ok(HookState::Foreign(p)) => format!("Not ours — `core.hooksPath` belongs to `{p}`"),
Err(e) => format!("Unknown ({e})"),
}
}
fn print_report(report: &TrustReport) {
output::print_header(&format!("What dev-prune {} may do", constants::VERSION));
println!();
println!(" Guaranteed by the code, on every machine");
println!();
for row in &report.guarantees {
print_row(row);
}
println!();
println!(" On this machine");
println!();
for row in &report.machine {
print_row(row);
}
println!();
let widened = report.widened();
if widened.is_empty() {
output::print_success(
"Nothing on this machine widens what dev-prune may do without asking.",
);
} else {
output::print_info(&format!(
"{} {} what dev-prune may do without asking: {}. Each was switched on \
deliberately; `devp config show` has them.",
widened.len(),
if widened.len() == 1 {
"setting widens"
} else {
"settings widen"
},
widened.join(", ")
));
}
output::print_info(
"The guarantees above are enforced in `src/engine.rs` and described in full at \
docs/SAFETY_INVARIANTS.md. None of them has a bypass flag.",
);
}
fn print_row(row: &TrustRow) {
println!(
" {} {:<30} {}",
row.verdict.mark(),
row.subject,
row.state
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_machine_widens_nothing() {
let registry = Registry::default();
let report = build(®istry);
assert!(
report.widened().is_empty(),
"a fresh install reports {:?} as widened",
report.widened()
);
}
#[test]
fn every_widening_setting_shows_up_by_name() {
let mut registry = Registry::default();
registry.settings.auto_update = true;
registry.settings.require_confirmation = false;
registry.settings.allow_manifest_rewrite = true;
registry.settings.enable_gradle = true;
let report = build(®istry);
let widened = report.widened();
assert_eq!(widened.len(), 4, "got {widened:?}");
assert!(widened.contains(&"Auto-update"));
assert!(widened.contains(&"Opt-in adapters"));
}
#[test]
fn opt_in_adapters_are_listed_in_a_stable_order() {
let mut registry = Registry::default();
registry.settings.enable_swift = true;
registry.settings.enable_gradle = true;
assert_eq!(opt_in_adapters(®istry), vec!["gradle", "swift"]);
}
#[test]
fn every_row_key_is_unique() {
let report = build(&Registry::default());
let mut keys: Vec<&str> = report
.guarantees
.iter()
.chain(report.machine.iter())
.map(|r| r.key)
.collect();
let total = keys.len();
keys.sort_unstable();
keys.dedup();
assert_eq!(keys.len(), total);
}
#[test]
fn guarantees_never_depend_on_settings() {
let mut registry = Registry::default();
registry.settings.allow_manifest_rewrite = true;
registry.settings.auto_update = true;
let with = build(®istry);
let without = build(&Registry::default());
let states = |r: &TrustReport| -> Vec<String> {
r.guarantees.iter().map(|g| g.state.clone()).collect()
};
assert_eq!(states(&with), states(&without));
}
}