Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/tracedecay-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.
tracedecay-sdk = { path = "../tracedecay-sdk", version = "0.1.0" }
tracedecay-session-memory = { path = "../tracedecay-session-memory", version = "0.1.0" }
tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0" }
tracedecay-store-runtime = { path = "../tracedecay-store-runtime", version = "0.1.0" }
tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1.0" }
tracing = "0.1"
ureq = { version = "3", features = ["json"] }
Expand Down Expand Up @@ -209,7 +210,6 @@ tracedecay-lcm = { path = "../tracedecay-lcm", version = "0.1.0" }
tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0", features = ["test-helpers"] }
tracedecay-semantic = { path = "../tracedecay-semantic", version = "0.1.0", features = ["test-helpers"] }
tracedecay-semantic-contracts.workspace = true
tracedecay-store-runtime = { path = "../tracedecay-store-runtime", version = "0.1.0" }
tracedecay-session-temporal-store = { path = "../tracedecay-session-temporal-store", version = "0.1.0" }
# Dev-only: `tests/work_route_exposure_conformance.rs` still asserts against
# the usecases `operation_stream` authority; production code is off usecases.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
//! on an async or deadline-bound path must run them on a blocking thread and
//! bound the join itself (see the CLI status command).

use std::cmp::Ordering;
use std::time::Duration;

use semver::Version;

/// The Cloudflare Worker endpoint URL.
const WORKER_URL: &str = "https://tracedecay-counter.enzinol.workers.dev";

Expand Down Expand Up @@ -248,41 +245,26 @@ pub fn is_beta() -> bool {
tracedecay_dashboard_api::cloud::is_beta(env!("CARGO_PKG_VERSION"))
}

/// Returns true if `latest` is strictly newer than `current` using `SemVer`
/// precedence (`Version::cmp_precedence`), so build metadata does not affect
/// ordering. Stable and beta remain separate channels: a prerelease never
/// dominates a stable release (or the reverse), even when the numeric core is
/// higher.
pub fn is_newer_version(current: &str, latest: &str) -> bool {
let Ok(current) = Version::parse(current) else {
return false;
};
let Ok(latest) = Version::parse(latest) else {
return false;
};
// Beta and stable are separate channels — never suggest cross-channel updates.
if current.pre.is_empty() != latest.pre.is_empty() {
return false;
/// Admits these ureq implementations into the composition library so MCP
/// flush/version checks can run after the binary starts. Unregistered
/// lookups stay `None` (already best-effort).
pub fn admit_sync_probes() {
tracedecay_dashboard_api::cloud::admit_sync_cloud_probes(flush_pending, fetch_latest_version);
}

pub fn doctor_network_probes() -> tracedecay::doctor::AdmittedDoctorNetworkProbes {
tracedecay::doctor::AdmittedDoctorNetworkProbes {
fetch_worldwide_total,
fetch_latest_version,
}
latest.cmp_precedence(&current) == Ordering::Greater
}

/// Returns true if `latest` is a newer version than `current` AND the
/// difference is at least a minor version bump (patch-only bumps return false).
///
/// Used by the CLI version warning to avoid nagging on patch releases.
pub fn is_newer_version(current: &str, latest: &str) -> bool {
tracedecay_dashboard_api::cloud::is_newer_version(current, latest)
}

pub fn is_newer_minor_version(current: &str, latest: &str) -> bool {
let Ok(current) = Version::parse(current) else {
return false;
};
let Ok(latest) = Version::parse(latest) else {
return false;
};
if current.pre.is_empty() != latest.pre.is_empty() {
return false;
}
latest.cmp_precedence(&current) == Ordering::Greater
&& (latest.major, latest.minor) > (current.major, current.minor)
tracedecay_dashboard_api::cloud::is_newer_minor_version(current, latest)
}

/// How tracedecay was installed, detected from the binary path.
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ pub(crate) use settings::{
};
pub(crate) use storage::{
ProfileOfflineAuthority, handle_list, handle_wipe, join_outcome_and_restore,
take_profile_offline,
take_profile_offline, try_admit_profile_registry,
};
2 changes: 1 addition & 1 deletion crates/tracedecay-cli/src/commands/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ pub(crate) async fn handle_sync(
resolved.project_path.display()
);
if doctor {
tracedecay::doctor::run_doctor().await?;
tracedecay::doctor::run_doctor(crate::cloud::doctor_network_probes()).await?;
}
Ok(())
}
71 changes: 54 additions & 17 deletions crates/tracedecay-cli/src/commands/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::io::{self, BufRead, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;

use tracedecay::profile_registry_maintenance::{remove_store_directory, verify_store_path_absent};
use tracedecay_global_db::profile_registry_maintenance::{
ProfileRegistryMaintenanceRuntime, remove_store_directory, verify_store_path_absent,
};

use crate::global;

Expand Down Expand Up @@ -178,6 +180,45 @@ impl ProfileOfflineAuthority {
}
}

/// Opens an existing exact-final profile registry without creating one, then
/// admits the lease into [`ProfileRegistryMaintenanceRuntime`].
pub(crate) async fn try_admit_profile_registry(
profile_root: &Path,
) -> tracedecay_domain::errors::Result<Option<ProfileRegistryMaintenanceRuntime>> {
if !profile_root.try_exists().map_err(|error| {
tracedecay_domain::errors::TraceDecayError::Database {
operation: "inspect existing profile root".to_string(),
message: error.to_string(),
}
})? {
return Ok(None);
}
let profile_root = profile_root.canonicalize().map_err(|error| {
tracedecay_domain::errors::TraceDecayError::Database {
operation: "resolve existing profile registry".to_string(),
message: error.to_string(),
}
})?;
if !profile_root
.join("global.db")
.try_exists()
.map_err(
|error| tracedecay_domain::errors::TraceDecayError::Database {
operation: "inspect existing profile registry".to_string(),
message: error.to_string(),
},
)?
{
return Ok(None);
}
let identity = tracedecay_daemon_identity::profile_identity::load_existing(&profile_root)?;
let registry = tracedecay_store_runtime::DaemonSessionRuntimeRegistryV1::open(identity).await?;
let profile_database = registry.profile_database().await?;
Ok(Some(
ProfileRegistryMaintenanceRuntime::from_admitted_lease(profile_database),
))
}

/// Takes the whole profile offline for a destructive maintenance command,
/// within a bound, or refuses typed — never "retry after it finishes".
///
Expand Down Expand Up @@ -428,8 +469,7 @@ async fn wipe_under_profile_offline(
let registry = if all {
None
} else {
tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime::try_open_existing(profile_root)
.await?
try_admit_profile_registry(profile_root).await?
};

// A complete wipe is deliberately schema-independent: the databases may
Expand Down Expand Up @@ -632,17 +672,16 @@ fn handle_list_inner(
} else {
0
};
let project_key =
tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime::canonical_project_key(path);
let project_key = tracedecay_global_db::RegisteredGlobalDb::canonical_project_key(path);
let token_row = token_rows.iter().find(|row| {
row.get("project")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| {
tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime::canonical_project_key(
Path::new(value),
) == project_key
})
});
row.get("project")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| {
tracedecay_global_db::RegisteredGlobalDb::canonical_project_key(Path::new(
value,
)) == project_key
})
});
// `None` is a total this run could not read, which is not the same
// answer as a project that has saved nothing.
let tokens = token_row
Expand Down Expand Up @@ -762,9 +801,7 @@ fn append_orphan_manifest_rows(
};
let registered: std::collections::HashSet<String> = project_paths
.iter()
.map(|path| {
tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime::canonical_project_key(path)
})
.map(|path| tracedecay_global_db::RegisteredGlobalDb::canonical_project_key(path))
.collect();
let report = tracedecay_global_db::registry_maintenance::inspect_profile_store_orphans(
profile_root,
Expand All @@ -776,7 +813,7 @@ fn append_orphan_manifest_rows(
{
continue;
}
let key = tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime::canonical_project_key(
let key = tracedecay_global_db::RegisteredGlobalDb::canonical_project_key(
&plan.project.project_root,
);
if registered.contains(&key) {
Expand Down
12 changes: 7 additions & 5 deletions crates/tracedecay-cli/src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub(crate) fn classify_project_storage(project_root: &Path) -> ProjectStorageLoc

pub(crate) async fn classify_project_storage_with_registry(
project_root: &Path,
registry: Option<&tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime>,
registry: Option<
&tracedecay_global_db::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime,
>,
profile_root: Option<&Path>,
) -> tracedecay_domain::errors::Result<ProjectStorageLocation> {
let location = classify_project_storage(project_root);
Expand Down Expand Up @@ -87,7 +89,7 @@ pub(crate) fn try_flush(
}

config.last_flush_attempt_at = now;
if let Some(worldwide_total) = tracedecay::cloud::flush_pending(config.pending_upload) {
if let Some(worldwide_total) = crate::cloud::flush_pending(config.pending_upload) {
config.pending_upload = 0;
config.last_upload_at = now;
config.last_worldwide_total = worldwide_total;
Expand All @@ -112,7 +114,7 @@ pub(crate) fn check_for_update(
return;
}
config.cached_latest_version.clone()
} else if let Some(v) = tracedecay::cloud::fetch_latest_version() {
} else if let Some(v) = crate::cloud::fetch_latest_version() {
config.cached_latest_version = v.clone();
config.last_version_check_at = now;
if let Err(err) = config.save_if_exists() {
Expand All @@ -126,9 +128,9 @@ pub(crate) fn check_for_update(
// The status page (skip_suppression=true) warns on any newer version;
// the CLI only warns on minor+ bumps to avoid nagging on patch releases.
let dominated = if skip_suppression {
tracedecay::cloud::is_newer_version(current_version, &latest)
crate::cloud::is_newer_version(current_version, &latest)
} else {
tracedecay::cloud::is_newer_minor_version(current_version, &latest)
crate::cloud::is_newer_minor_version(current_version, &latest)
};

if dominated && (skip_suppression || elapsed_since(now, config.last_version_warning_at) >= 900)
Expand Down
8 changes: 7 additions & 1 deletion crates/tracedecay-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod agent_cmd;
mod analytics_cmd;
mod automation_cli;
mod cli;
mod cloud;
mod commands;
mod cost_cmd;
mod display;
Expand Down Expand Up @@ -617,6 +618,7 @@ fn async_main() -> tracedecay_domain::errors::Result<CommandOutcome> {
// dashboard bundle; the composition library reads both through this
// set-once registration.
tracedecay::register_product_runtime(crate::product_runtime::provider())?;
crate::cloud::admit_sync_probes();
// Every process-global runtime port the extracted crates invert back into
// the composition root. Must precede argument parsing: hook, install, and
// ingest paths all read these slots, and an unregistered slot fails quietly
Expand Down Expand Up @@ -1843,7 +1845,11 @@ async fn dispatch_configuration_command(
async fn dispatch_diagnostics_command(command: Commands) -> tracedecay_domain::errors::Result<()> {
match command {
Commands::Doctor => {
hotpath::future!(tracedecay::doctor::run_doctor(), label = "cli.doctor.run").await?;
hotpath::future!(
tracedecay::doctor::run_doctor(crate::cloud::doctor_network_probes()),
label = "cli.doctor.run"
)
.await?;
}
Commands::Cost {
range,
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-cli/src/product_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub(crate) fn register_for_tests() {
Ok(()) | Err(tracedecay::ProductRuntimeError::ConflictingProvider) => {}
Err(error) => panic!("register the CLI product runtime for tests: {error}"),
}
crate::cloud::admit_sync_probes();
}

pub(crate) fn provider() -> tracedecay::ProductRuntimeProvider {
Expand Down
9 changes: 5 additions & 4 deletions crates/tracedecay-cli/src/project_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ use std::fmt::Write as _;
use std::path::{Path, PathBuf};

use serde_json::{Value, json};
use tracedecay::profile_registry_maintenance::ProfileRegistryMaintenanceRuntime;
use tracedecay_contracts::{ProjectRegistryView, render_project_registry_view};
use tracedecay_domain::errors::{Result, TraceDecayError};
#[cfg(test)]
use tracedecay_global_db::ProjectRegistryContext;
use tracedecay_global_db::RegisteredGlobalDb;

use crate::cli::ProjectsAction;
use crate::commands::{ProfileOfflineAuthority, join_outcome_and_restore, take_profile_offline};
use crate::commands::{
ProfileOfflineAuthority, join_outcome_and_restore, take_profile_offline,
try_admit_profile_registry,
};

const MAX_LIMIT: usize = 1_000;

Expand Down Expand Up @@ -129,8 +131,7 @@ async fn forget_under_profile_offline(
profile_root,
"projects forget",
)?;
let Some(registry) = ProfileRegistryMaintenanceRuntime::try_open_existing(profile_root).await?
else {
let Some(registry) = try_admit_profile_registry(profile_root).await? else {
return Err(TraceDecayError::Config {
message: "no profile registry exists; there is nothing to forget".to_string(),
});
Expand Down
4 changes: 2 additions & 2 deletions crates/tracedecay-cli/src/status_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ impl OnlineRefreshPlan {
OnlineRefresh {
worldwide_total: self
.worldwide_total
.then(tracedecay::cloud::fetch_worldwide_total)
.then(crate::cloud::fetch_worldwide_total)
.flatten(),
country_flags: if self.country_flags {
tracedecay::cloud::fetch_country_flags()
crate::cloud::fetch_country_flags()
} else {
Vec::new()
},
Expand Down
8 changes: 4 additions & 4 deletions crates/tracedecay-cli/src/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::time::{Duration, Instant};
use sha2::{Digest, Sha256};
use tempfile::TempDir;

use tracedecay::cloud::{self, InstallMethod};
use crate::cloud::{self, InstallMethod};
use tracedecay_domain::errors::{Result, TraceDecayError};
use tracedecay_runtime_core::git::{GitCommandBounds, GitCommandError, bounded_command_output};
use tracedecay_session_memory::user_config::UserConfig;
Expand Down Expand Up @@ -115,12 +115,12 @@ impl StagedRelease {
}
}

// Asset-naming and platform helpers live in `tracedecay::cloud` so the version-
// Asset-naming and platform helpers live in `crate::cloud` so the version-
// detection path can use the same naming convention to filter out releases
// whose CI hasn't finished uploading the current platform's binary yet.
use tracedecay::cloud::asset_name;
use crate::cloud::asset_name;
#[cfg(test)]
use tracedecay::cloud::current_platform;
use crate::cloud::current_platform;

/// The GitHub release tag for a given version.
fn release_tag(version: &str) -> String {
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-dashboard-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ axum = "0.8"
clru = "0.6"
hotpath.workspace = true
schemars = "1.2.1"
semver = "1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove semver from the former owner

With the version-comparison implementation and its semver import moved into tracedecay-dashboard-api, a repository-wide search finds no remaining semver usage under crates/tracedecay, but crates/tracedecay/Cargo.toml:363 still declares it as a direct dependency. Remove that stale declaration so the composition-root manifest reflects its actual crate boundary and does not retain an unused dependency after the cutover.

AGENTS.md reference: AGENTS.md:L113-L120

Useful? React with 👍 / 👎.

serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.11"
Expand Down
Loading
Loading