Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
456a5e9
fix(storage): reject unreadable profile registries
ScriptedAlchemy Nov 8, 2026
6df1dce
style(storage): format registry-report regression
ScriptedAlchemy Nov 8, 2026
d2ea4f5
fix(storage): refuse linked foreign registries in footprint reports
ScriptedAlchemy Nov 8, 2026
43cef92
style(storage): format linked-registry refusal test
ScriptedAlchemy Nov 8, 2026
8b403d1
fix(storage): stream table growth census without page sorting
ScriptedAlchemy Nov 8, 2026
d9559c8
style(storage): align census regressions with pinned rustfmt
ScriptedAlchemy Nov 8, 2026
679f636
test(storage): satisfy clippy for empty registry assertion
ScriptedAlchemy Nov 8, 2026
a3654ac
fix(storage): measure user tables with sqlite-like names
ScriptedAlchemy Nov 8, 2026
4bcc943
style(storage): apply pinned rustfmt to growth regression
ScriptedAlchemy Nov 8, 2026
1e3d62f
fix(storage): reject linked SQLite report sidecars
ScriptedAlchemy Nov 8, 2026
8913817
style(storage): apply pinned rustfmt to snapshot regression
ScriptedAlchemy Nov 8, 2026
eb751ef
fix(storage): capture registry generation before WAL discovery
ScriptedAlchemy Nov 8, 2026
f982768
fix(storage): pin SQLite members against symlink swaps
ScriptedAlchemy Nov 8, 2026
2d839d8
fix(storage): refuse replaced SQLite report members before copy
ScriptedAlchemy Nov 8, 2026
7f54a84
style(storage): honor top-level Unix metadata imports
ScriptedAlchemy Nov 8, 2026
32aa388
fix(storage): refuse rollback-journal registry snapshots
ScriptedAlchemy Nov 8, 2026
c4258d8
style(storage): format SQLite snapshot regressions
ScriptedAlchemy Nov 8, 2026
7990e35
fix(storage): recognize inert rollback journals in reports
ScriptedAlchemy Nov 8, 2026
0c7597a
style(storage): align journal probe with pinned rustfmt
ScriptedAlchemy Nov 8, 2026
c396dee
fix(storage): satisfy Clippy for rollback journal checks
ScriptedAlchemy Nov 8, 2026
e64168b
perf(storage): scan each SQLite page once for table growth
ScriptedAlchemy Nov 8, 2026
1b9bf85
fix(storage): retain borrowed dbstat value through lookup
ScriptedAlchemy Nov 8, 2026
bb53e20
style(storage): align single-pass census with pinned rustfmt
ScriptedAlchemy Nov 8, 2026
2fdaffb
fix(storage): count retained SQLite journals in family sizes
ScriptedAlchemy Nov 8, 2026
dcca6bc
test(automation): bind scheduler fixtures to backend identities
ScriptedAlchemy Nov 8, 2026
1a80a56
fix(doctor): retain footprint when registry enumeration fails
ScriptedAlchemy Nov 8, 2026
edb27a3
test(doctor): use stable missing-root fixture
ScriptedAlchemy Nov 8, 2026
67a0121
style(doctor): make footprint imports explicit
ScriptedAlchemy Nov 8, 2026
abd9956
style(doctor): format registered footprint handling
ScriptedAlchemy Nov 8, 2026
0bb47ea
fix(storage): refuse malformed registry project paths
ScriptedAlchemy Nov 9, 2026
aa507c4
style(storage): format malformed registry regression
ScriptedAlchemy Nov 9, 2026
cec4106
fix(storage): re-probe table growth after daemon restart
ScriptedAlchemy Nov 9, 2026
bcd6655
fix(storage): attribute graph sidecars to their container
ScriptedAlchemy Nov 9, 2026
ad8d085
style(storage): keep graph sidecar classification precise
ScriptedAlchemy Nov 9, 2026
d992d04
fix(doctor): preserve long-path footprint findings
ScriptedAlchemy Nov 9, 2026
44bea7e
simplify(doctor): reuse canonical footprint digest
ScriptedAlchemy Nov 9, 2026
55ef65e
fix(doctor): retain the cause of partial hashed findings
ScriptedAlchemy Nov 9, 2026
385fa23
fix(storage): include graph sidecars in Doctor container bounds
ScriptedAlchemy Nov 9, 2026
d9faf82
fix(storage): exclude unowned graph suffixes from container bounds
ScriptedAlchemy Nov 9, 2026
4b542ed
fix(storage): include spill directories in graph container bounds
ScriptedAlchemy Nov 9, 2026
4125cc9
fix(storage): deduplicate unlinked SQLite TEMP descriptors
ScriptedAlchemy Nov 9, 2026
04fcc25
Merge master into fix/4099-report-registry-presence-20261107
ScriptedAlchemy Nov 9, 2026
acfe68e
fix(storage): separate unrelated SQLite suffix footprint bytes
ScriptedAlchemy Nov 9, 2026
b60cee7
fix(storage): classify only owned project database sidecars
ScriptedAlchemy Nov 9, 2026
68e8021
fix(storage): isolate unrelated profile memory suffix files
ScriptedAlchemy Nov 9, 2026
7756fa7
fix(storage): exclude SQLite sidecar lookalike directories
ScriptedAlchemy Nov 9, 2026
82384ba
fix(storage): keep WAL-named payloads in their owner bounds
ScriptedAlchemy Nov 9, 2026
70f5596
fix(storage): charge WAL-named spill files to graph containers
ScriptedAlchemy Nov 9, 2026
ff8011d
fix(storage): keep WAL-named host files under owner bounds
ScriptedAlchemy Nov 9, 2026
a7fc901
fix(storage): resolve SQLite family helper collision
ScriptedAlchemy Nov 9, 2026
9c8ed6c
fix(storage): preserve project-owned nested WAL-looking payloads
ScriptedAlchemy Nov 9, 2026
b7bfa9d
fix(storage): distinguish SQLite WALs from lookalike directories
ScriptedAlchemy Nov 9, 2026
092ba0e
style(storage): align WAL classifier and regression formatting
ScriptedAlchemy Nov 9, 2026
cdf1228
style(storage): satisfy pinned footprint rustfmt check
ScriptedAlchemy Nov 9, 2026
a1513ef
fix(storage): retain project root partial findings
ScriptedAlchemy Nov 9, 2026
22bfc69
style(storage): align project-root regression formatting
ScriptedAlchemy Nov 9, 2026
fb4b65f
style(storage): satisfy pinned footprint rustfmt (Refs #4099)
ScriptedAlchemy Nov 9, 2026
1825f4a
test(storage): keep hashed WAL diagnostic test valid
ScriptedAlchemy Nov 9, 2026
7e02189
fix(storage): expose replaced project-root files in doctor
ScriptedAlchemy Nov 9, 2026
f0d9914
test(storage): exercise replacement bound without large disk allocation
ScriptedAlchemy Nov 9, 2026
7de8bcd
style(retention): apply rustfmt to footprint tally
ScriptedAlchemy Nov 9, 2026
95e7ad1
fix(storage): retry table census after freelist reclamation
ScriptedAlchemy Nov 9, 2026
6179c8b
test(storage): preserve growth baselines through reclamation
ScriptedAlchemy Nov 9, 2026
aaf3e6f
style(storage): apply Bazel-pinned Rust formatting
ScriptedAlchemy Nov 10, 2026
3610e38
fix(storage): refuse linked profile roots in footprint census
ScriptedAlchemy Nov 10, 2026
607a496
fix(storage): refuse linked roots before report sampling
ScriptedAlchemy Nov 10, 2026
830e9cb
test(storage): prove linked roots refuse real registry sampling
ScriptedAlchemy Nov 10, 2026
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
150 changes: 140 additions & 10 deletions crates/tracedecay-daemon-service/src/doctor_kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! `Unsupported`/`Absent`/`Denied`/`Unknown` read, never a fabricated healthy
//! result, and partial coverage carries its real reason.
//!
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracedecay_application::advisory::github_runtime::{
Expand Down Expand Up @@ -44,6 +44,9 @@ use tracedecay_domain::CodeGenerationId;
use tracedecay_global_db::{GlobalDbNativeIntegrationStore, RegisteredGlobalDb};

use crate::DaemonFeedbackRuntimeRegistrar;
use tracedecay_maintenance::retention::footprint::{
ProfileFootprintV1, ProjectRootStateV1, census_profile_footprint, census_sqlite_temp_spill,
};
use tracedecay_maintenance::telemetry::GuardedStoreTelemetryPort;
use tracedecay_session_temporal_store::SessionTemporalAccess;

Expand Down Expand Up @@ -614,28 +617,53 @@ fn soft_budget_findings(
storage_family_read(findings)
}

/// A failed registry listing must not hide the physical profile footprint.
/// An unknown registry is not an empty registry: orphan classifications require
/// proven registration state, but file sizes and their bounds do not.
fn profile_footprint_from_registered_roots(
profile_root: &Path,
registered_roots: Option<&BTreeMap<String, String>>,
overrides: &BTreeMap<String, u64>,
) -> ProfileFootprintV1 {
let empty_roots = BTreeMap::new();
let mut footprint = census_profile_footprint(
profile_root,
registered_roots.unwrap_or(&empty_roots),
overrides,
);
if registered_roots.is_none() {
for entry in &mut footprint.entries {
if let Some(project) = entry.project.as_mut() {
project.state = ProjectRootStateV1::Unverifiable;
}
}
}
footprint
}

/// Size every store family under the profile against its bound. Registered
/// project roots name each project store's root and whether it still exists.
#[tracing::instrument(name = "daemon.doctor.profile_footprint", level = "trace", skip_all)]
async fn profile_footprint_read(
registry: &RegisteredGlobalDb,
profile_root: &Path,
retention: &tracedecay_configuration::RetentionConfig,
) -> Option<tracedecay_maintenance::retention::footprint::ProfileFootprintV1> {
) -> Option<ProfileFootprintV1> {
let registered = registry
.list_code_projects(usize::MAX)
.await
.ok()?
.into_iter()
.map(|project| (project.project_id, project.canonical_root))
.collect::<std::collections::BTreeMap<_, _>>();
.ok()
.map(|projects| {
projects
.into_iter()
.map(|project| (project.project_id, project.canonical_root))
.collect::<BTreeMap<_, _>>()
});
let overrides = retention.store_soft_budgets_bytes.clone();
let profile_root = profile_root.to_path_buf();
tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
use tracedecay_maintenance::retention::footprint::{
census_profile_footprint, census_sqlite_temp_spill,
};
let mut footprint = census_profile_footprint(&profile_root, &registered, &overrides);
let mut footprint =
profile_footprint_from_registered_roots(&profile_root, registered.as_ref(), &overrides);
footprint.entries.extend(census_sqlite_temp_spill(
Path::new("/proc/self/fd"),
&overrides,
Expand All @@ -646,6 +674,108 @@ async fn profile_footprint_read(
.ok()
}

#[cfg(test)]
mod profile_footprint_tests {
use std::collections::BTreeMap;
use tracedecay_contracts::doctor::{DoctorEvidenceStateV1, DoctorStorageFamilyReadV1};
use tracedecay_maintenance::retention::footprint::{
ProfileFamilyV1, ProjectRootStateV1, footprint_findings,
};

use super::profile_footprint_from_registered_roots;

fn state_for_store(
profile: &tracedecay_maintenance::retention::footprint::ProfileFootprintV1,
) -> ProjectRootStateV1 {
profile
.entries
.iter()
.find(|entry| entry.key == "projects/proj_growing")
.and_then(|entry| entry.project.as_ref())
.expect("measured project store")
.state
}

#[test]
fn unreadable_registry_cannot_hide_large_profile_or_mislabel_orphans() {
let temp = tempfile::tempdir().unwrap();
let profile_root = temp.path().join("profile");
let store_root = profile_root.join("projects/proj_growing");
std::fs::create_dir_all(&store_root).unwrap();
std::fs::create_dir_all(profile_root.join("lcm-payloads")).unwrap();
std::fs::write(store_root.join("tracedecay.db-wal"), vec![0; 4096]).unwrap();
std::fs::write(profile_root.join("lcm-payloads/blob"), vec![0; 2048]).unwrap();

let overrides = BTreeMap::from([
("projects/proj_growing".to_owned(), 1024),
("profile".to_owned(), 1024),
]);
// None is a failed registry read, not a successful empty listing.
let unknown = profile_footprint_from_registered_roots(&profile_root, None, &overrides);
assert_eq!(unknown.total_bytes, 6144);
assert_eq!(state_for_store(&unknown), ProjectRootStateV1::Unverifiable);
let project = unknown
.entries
.iter()
.find(|entry| entry.key == "projects/proj_growing")
.unwrap();
assert_eq!(project.family, ProfileFamilyV1::ProjectStore);
assert_eq!(project.bytes, 4096);
assert_eq!(project.bound_bytes, 1024);
assert_eq!(project.project.as_ref().unwrap().registered_root, None);

let DoctorStorageFamilyReadV1::Observed { findings } = footprint_findings(&unknown) else {
panic!("physical footprint must remain observable without a registry");
};
let named = findings
.into_iter()
.map(|finding| {
(
finding.finding().evidence()[0]
.reference()
.as_str()
.to_owned(),
finding.finding().state(),
)
})
.collect::<BTreeMap<_, _>>();
assert_eq!(
named.get("projects/proj_growing"),
Some(&DoctorEvidenceStateV1::Degraded)
);
assert_eq!(named.get("profile"), Some(&DoctorEvidenceStateV1::Degraded));

// Distinguish failed enumeration from an authoritative empty registry,
// and from a registered root whose filesystem directory was deleted.
let empty = BTreeMap::new();
let unregistered =
profile_footprint_from_registered_roots(&profile_root, Some(&empty), &overrides);
assert_eq!(
state_for_store(&unregistered),
ProjectRootStateV1::Unregistered
);
let deleted_root = temp.path().join("deleted-worktree");
let registered = BTreeMap::from([(
"proj_growing".to_owned(),
deleted_root.display().to_string(),
)]);
let missing =
profile_footprint_from_registered_roots(&profile_root, Some(&registered), &overrides);
assert_eq!(state_for_store(&missing), ProjectRootStateV1::Missing);
assert_eq!(
missing
.entries
.iter()
.find(|entry| entry.key == "projects/proj_growing")
.and_then(|entry| entry.project.as_ref())
.and_then(|project| project.registered_root.as_ref())
.map(String::as_str),
deleted_root.to_str()
);
assert!(!deleted_root.exists());
}
}

/// The generations the retention pass protects: the one the mounted scheduler
/// serves, through either the sealed seat or the text projection, and every
/// live native-preview candidate binding of the repository. This is the set
Expand Down
Loading
Loading