Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 211 additions & 8 deletions crates/tracedecay-maintenance/src/retention/footprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,26 @@ fn floor_note(unavailable_entry_count: usize, truncated: bool) -> String {
}
}

/// Only an enrolled project has a first-party destructive reclaim path.
/// An unregistered directory or unrelated profile family must never be sent
/// through `projects forget`; it only accepts registered identities.
fn registered_project_reclaim_hint(entry: &ProfileFootprintEntryV1) -> Option<String> {
if entry.family != ProfileFamilyV1::ProjectStore || entry.bytes <= entry.bound_bytes {
return None;
}
entry.project.as_ref()?.registered_root.as_ref()?;
let id = entry.key.strip_prefix("projects/").filter(|id| {
id.starts_with("proj_")
&& id.len() > "proj_".len()
&& id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
})?;
Some(format!(
"; if retention stalls, LAST RESORT (DELETES project data): `tracedecay --dry-run projects forget {id}` then `tracedecay --yes projects forget {id}`"
))
}

fn entry_statement(entry: &ProfileFootprintEntryV1) -> String {
let label = match entry.family {
ProfileFamilyV1::ProjectStore => "project store",
Expand Down Expand Up @@ -609,10 +629,40 @@ fn entry_statement(entry: &ProfileFootprintEntryV1) -> String {
format_bytes(kinds.graph_replay_pool)
)
});
let reclaim = registered_project_reclaim_hint(entry).unwrap_or_default();
let floor = floor_note(entry.unavailable_entry_count, false);
let render = |root: &str, reclaim: &str| {
format!("{label} {}{reclaim}{root}{breakdown}{floor}", entry.key)
};
let fits = |subject: &str| {
let statement = footprint_statement(subject, entry.bytes, entry.bound_bytes);
super::diagnostics::bounded_statement(&statement) == statement
};
let detailed = render(&root, &reclaim);
if reclaim.is_empty() || fits(&detailed) {
return detailed;
}
// Budget the complete rendered finding, including both commands, the
// deletion warning, and the measured overage. A root-length heuristic
// does not account for selector length, UTF-8, or the other evidence.
let compact = render(
" (registered root omitted from bounded diagnostic)",
&reclaim,
);
if fits(&compact) {
compact
} else {
// Never turn a long identity into a truncated destructive command.
// Keep the pressure evidence but omit an action that cannot fit whole.
render("", "")
}
}

fn footprint_statement(subject: &str, bytes: u64, bound_bytes: u64) -> String {
format!(
"{label} {}{root}{breakdown}{}",
entry.key,
floor_note(entry.unavailable_entry_count, false)
"{subject}: {} of {} bound",
format_bytes(bytes),
format_bytes(bound_bytes)
)
}

Expand All @@ -639,11 +689,7 @@ fn bounded_finding(
DoctorCoverageCompletenessV1::Complete,
)
};
let statement = format!(
"{subject}: {} of {} bound",
format_bytes(bytes),
format_bytes(bound_bytes)
);
let statement = footprint_statement(subject, bytes, bound_bytes);
let reference = DoctorEvidenceReferenceV1::new(key.to_owned()).ok()?;
let evidence = DoctorEvidenceRefV1::new(DoctorFindingFamilyV1::Storage, reference);
let coverage = DoctorCoverageStatementV1::new(
Expand Down Expand Up @@ -997,6 +1043,163 @@ mod tests {
);
}

#[test]
fn overbound_project_doctor_names_a_supported_destructive_reclaim_only_for_registered_stores() {
let fixture = profile_fixture();
let overrides = BTreeMap::from([
("projects/proj_live".to_owned(), 1024),
("projects/proj_stray".to_owned(), 1),
]);
let mut over = census_profile_footprint(&fixture.profile, &fixture.registered, &overrides);
let over_findings = findings(&over);
let live = &over_findings["projects/proj_live"];
assert_eq!(live.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = live.finding().coverage().statement();
assert!(
statement.contains("tracedecay --dry-run projects forget proj_live"),
"{statement}"
);
assert!(
statement.contains("tracedecay --yes projects forget proj_live"),
"{statement}"
);
assert!(statement.contains("DELETES project data"), "{statement}");

let unregistered = &over_findings["projects/proj_stray"];
assert_eq!(
unregistered.finding().state(),
DoctorEvidenceStateV1::Degraded
);
assert!(
!unregistered
.finding()
.coverage()
.statement()
.contains("projects forget"),
"an unregistered directory has no safe project selector"
);
assert!(
!over_findings["profile"]
.finding()
.coverage()
.statement()
.contains("projects forget"),
"a profile total does not identify a reclaimable project"
);

let long_root = "nested-repository/".repeat(70);
let registered = over
.entries
.iter_mut()
.find(|entry| entry.key == "projects/proj_live")
.expect("registered footprint");
registered
.project
.as_mut()
.expect("registered project")
.registered_root = Some(long_root);
let bounded = findings(&over);
let bounded_statement = bounded["projects/proj_live"]
.finding()
.coverage()
.statement();
assert!(
bounded_statement.contains("tracedecay --dry-run projects forget proj_live"),
"{bounded_statement}"
);
assert!(
bounded_statement.contains(" of 1.0 KB bound"),
"over-budget bytes and limit must survive a long repository path: {bounded_statement}"
);
assert!(bounded_statement.contains("registered root omitted"));

let normal =
census_profile_footprint(&fixture.profile, &fixture.registered, &BTreeMap::new());
let healthy = findings(&normal);
assert_eq!(
healthy["projects/proj_live"].finding().state(),
DoctorEvidenceStateV1::HealthyCompleteCoverage
);
assert!(
!healthy["projects/proj_live"]
.finding()
.coverage()
.statement()
.contains("projects forget"),
"within-budget stores must not be offered destructive recovery"
);
}

#[test]
fn reclaim_guidance_omits_unusable_project_selectors() {
let fixture = profile_fixture();
for id in [
"proj_".to_owned(),
"proj_bad;echo".to_owned(),
"proj_bad`id".to_owned(),
format!("proj_{}", "a".repeat(160)),
] {
let key = format!("projects/{id}");
let store = fixture.profile.join(&key).join("tracedecay.db");
sized(&store, 2048);
let registered = BTreeMap::from([(id, fixture.live_root.clone())]);
let overrides = BTreeMap::from([(key.clone(), 1)]);
let footprint = census_profile_footprint(&fixture.profile, &registered, &overrides);
let observed = findings(&footprint);
let finding = &observed[&key];
assert_eq!(finding.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = finding.finding().coverage().statement();
assert!(
!statement.contains("projects forget"),
"unsafe or incomplete actions must be omitted: {statement}"
);
assert!(statement.contains(" of 1 B bound"), "{statement}");
assert_eq!(
std::fs::metadata(&store).expect("unchanged store").len(),
2048
);
}
}

#[test]
fn reclaim_guidance_preserves_real_identity_commands_and_large_store_evidence() {
let temporary = tempfile::tempdir().expect("large footprint fixture");
let profile = temporary.path().join("profile");
let id = "proj_0123456789abcdef";
let key = format!("projects/{id}");
let store = profile.join(&key).join("tracedecay.db");
// Sparse metadata-only input: no allocation or read of the 15 GiB body.
sized(&store, 15 * GIB);
let registered = BTreeMap::from([(id.to_owned(), "界/".repeat(100))]);
let footprint = census_profile_footprint(&profile, &registered, &BTreeMap::new());
let observed = findings(&footprint);
let finding = &observed[&key];
assert_eq!(finding.finding().state(), DoctorEvidenceStateV1::Degraded);
let statement = finding.finding().coverage().statement();
assert!(
statement.contains(&format!("`tracedecay --dry-run projects forget {id}`")),
"{statement}"
);
assert!(
statement.contains(&format!("`tracedecay --yes projects forget {id}`")),
"{statement}"
);
assert!(statement.contains("DELETES project data"), "{statement}");
assert!(statement.contains("registered root omitted"), "{statement}");
assert!(
statement.ends_with(&format!(
"{} of {} bound",
format_bytes(15 * GIB),
format_bytes(default_bound_bytes(ProfileFamilyV1::ProjectStore))
)),
"{statement}"
);
assert_eq!(
std::fs::metadata(&store).expect("unchanged store").len(),
15 * GIB
);
}

#[test]
fn an_owner_override_replaces_the_default_bound() {
let fixture = profile_fixture();
Expand Down
Loading