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
5 changes: 5 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/tracedecay-application/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ url = "2"
zeroize = "1.9.0"
tracedecay-contracts = { path = "../tracedecay-contracts", version = "0.1.0" }
tracedecay-automation = { path = "../tracedecay-automation", version = "0.1.0" }
tracedecay-daemon-protocol = { path = "../tracedecay-daemon-protocol", version = "0.1.0" }
# Grammar-free entry only: `markdown_structure` compiles with no language
# feature enabled, so section structure reaches retrieval without linking a
# tree-sitter bundle into this crate.
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-application/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub mod observation;
pub mod operation_stream;
pub mod pr_tracking;
pub mod primitives;
pub mod project_adoption;
pub mod project_open_authorization;
pub mod semantic_runtime;
pub mod settings_control;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,10 @@
use std::path::{Path, PathBuf};

use tracedecay_daemon_protocol::MovedStoreAdoption;

use tracedecay_domain::errors::{Result, TraceDecayError};
use tracedecay_global_db::RegisteredGlobalDb;
use tracedecay_runtime_core::storage::{self, StoreLayout};

use super::TraceDecay;

#[derive(Debug, Clone, PartialEq, Eq)]
struct MovedNongitCandidate {
project_id: String,
Expand All @@ -51,114 +48,111 @@ enum MovedStoreEvidence {
NoMatch,
}

impl TraceDecay {
/// Remaps a moved non-git project onto `project_root` under an explicit
/// operator adoption decision.
///
/// Returns `Ok(None)` when adoption was not requested or there is no
/// moved-store candidate, so first-touch may mint a new identity.
/// Ambiguous or conflicting adoption is a typed refusal, never an alias.
#[hotpath::measure(label = "lifecycle.adopt_moved_nongit", future = true)]
pub(crate) async fn adopt_moved_nongit_project(
project_root: &Path,
profile_root: &Path,
registry: &RegisteredGlobalDb,
adoption: &MovedStoreAdoption,
) -> Result<Option<StoreLayout>> {
if matches!(adoption, MovedStoreAdoption::Never) {
return Ok(None);
}
if tracedecay_runtime_core::worktree::git_common_dir(project_root).is_some() {
return Ok(None);
}
/// Remaps a moved non-git project onto `project_root` under an explicit
/// operator adoption decision.
///
/// Returns `Ok(None)` when adoption was not requested or there is no
/// moved-store candidate, so first-touch may mint a new identity.
/// Ambiguous or conflicting adoption is a typed refusal, never an alias.
#[hotpath::measure(label = "lifecycle.adopt_moved_nongit", future = true)]
pub async fn adopt_moved_nongit_project(
project_root: &Path,
profile_root: &Path,
registry: &RegisteredGlobalDb,
adoption: &MovedStoreAdoption,
) -> Result<Option<StoreLayout>> {
if matches!(adoption, MovedStoreAdoption::Never) {
return Ok(None);
}
if tracedecay_runtime_core::worktree::git_common_dir(project_root).is_some() {
return Ok(None);
}

let new_root = project_root
.canonicalize()
.map_err(|error| TraceDecayError::Config {
message: format!(
"could not canonicalize moved-project adoption root '{}': {error}",
project_root.display()
),
})?;
let new_root = project_root
.canonicalize()
.map_err(|error| TraceDecayError::Config {
message: format!(
"could not canonicalize moved-project adoption root '{}': {error}",
project_root.display()
),
})?;

if let Some(existing) = registry
.project_registry_context_by_alias(&new_root)
.await?
{
return refuse_if_adoption_conflicts(adoption, &existing.project.project_id, &new_root);
}
if let Some(existing) = registry
.project_registry_context_by_alias(&new_root)
.await?
{
return refuse_if_adoption_conflicts(adoption, &existing.project.project_id, &new_root);
}

let candidates =
discover_moved_nongit_candidates(&new_root, profile_root, registry).await?;
let resuming = candidates
.iter()
.filter(|candidate| candidate.records_new_root)
.collect::<Vec<_>>();
let selected = match adoption {
MovedStoreAdoption::Never => return Ok(None),
MovedStoreAdoption::AdoptNamed(requested) => {
match candidates
.iter()
.find(|candidate| &candidate.project_id == requested)
{
Some(candidate) => candidate,
None => {
return Err(TraceDecayError::Config {
message: format!(
"project '{requested}' is not a moved non-git store \
let candidates = discover_moved_nongit_candidates(&new_root, profile_root, registry).await?;
let resuming = candidates
.iter()
.filter(|candidate| candidate.records_new_root)
.collect::<Vec<_>>();
let selected = match adoption {
MovedStoreAdoption::Never => return Ok(None),
MovedStoreAdoption::AdoptNamed(requested) => {
match candidates
.iter()
.find(|candidate| &candidate.project_id == requested)
{
Some(candidate) => candidate,
None => {
return Err(TraceDecayError::Config {
message: format!(
"project '{requested}' is not a moved non-git store \
that can be adopted at '{}'",
new_root.display()
),
});
}
new_root.display()
),
});
}
}
MovedStoreAdoption::AdoptUnique => match (resuming.as_slice(), candidates.as_slice()) {
}
MovedStoreAdoption::AdoptUnique => match (resuming.as_slice(), candidates.as_slice()) {
(_, []) => return Ok(None),
// A store whose manifest already records this exact root is
// positive linkage; it outranks unlinked stale rows.
([resumable], _) => *resumable,
(_, [candidate]) => candidate,
_ => {
return Err(TraceDecayError::Config {
message: format!(
"moved non-git project adoption at '{}' is ambiguous \
(candidates: {}); re-run `tracedecay init` with \
--adopt-project <proj_id>, or with --fresh to mint a \
new project identity here",
new_root.display(),
candidate_ids(&candidates)
),
});
}
},
MovedStoreAdoption::OfferCandidates => {
match (resuming.as_slice(), candidates.as_slice()) {
(_, []) => return Ok(None),
// A store whose manifest already records this exact root is
// positive linkage; it outranks unlinked stale rows.
// Resuming an interrupted remap needs no flag: the store's
// manifest recording this root was written under a previous
// explicit adoption and is the journal record to replay.
([resumable], _) => *resumable,
(_, [candidate]) => candidate,
_ => {
return Err(TraceDecayError::Config {
message: format!(
"moved non-git project adoption at '{}' is ambiguous \
(candidates: {}); re-run `tracedecay init` with \
--adopt-project <proj_id>, or with --fresh to mint a \
new project identity here",
new_root.display(),
candidate_ids(&candidates)
),
});
}
},
MovedStoreAdoption::OfferCandidates => {
match (resuming.as_slice(), candidates.as_slice()) {
(_, []) => return Ok(None),
// Resuming an interrupted remap needs no flag: the store's
// manifest recording this root was written under a previous
// explicit adoption and is the journal record to replay.
([resumable], _) => *resumable,
_ => {
return Err(TraceDecayError::Config {
message: format!(
"a moved non-git store may belong at '{}' (candidates: {}); \
"a moved non-git store may belong at '{}' (candidates: {}); \
adoption rebinds a registered project identity and needs an \
explicit choice: re-run `tracedecay init` with \
--adopt-project <proj_id> (or --yes when exactly one \
candidate exists), or with --fresh to mint a new project \
identity here",
new_root.display(),
candidate_ids(&candidates)
),
});
}
new_root.display(),
candidate_ids(&candidates)
),
});
}
}
};
}
};

remap_moved_nongit_project(&new_root, profile_root, registry, selected).await
}
remap_moved_nongit_project(&new_root, profile_root, registry, selected).await
}

fn candidate_ids(candidates: &[MovedNongitCandidate]) -> String {
Expand Down Expand Up @@ -263,15 +257,16 @@ fn moved_store_evidence(
}
}
if layout.config_path.is_file() {
let config = crate::config::load_config_from_path(previous_root, &layout.config_path)
.map_err(|error| TraceDecayError::Config {
message: format!(
"cannot evaluate moved-store adoption evidence from '{}': {error}; \
let config =
tracedecay_configuration::load_config_from_path(previous_root, &layout.config_path)
.map_err(|error| TraceDecayError::Config {
message: format!(
"cannot evaluate moved-store adoption evidence from '{}': {error}; \
repair or remove the store config, or re-run `tracedecay init` \
with --fresh to mint a new identity without adoption",
layout.config_path.display()
),
})?;
layout.config_path.display()
),
})?;
let recorded = PathBuf::from(&config.root_dir);
if paths_record_same_root(&recorded, new_root) {
return Ok(MovedStoreEvidence::RecordsNewRoot);
Expand Down Expand Up @@ -328,9 +323,10 @@ async fn remap_moved_nongit_project(
),
})?
.to_owned();
let mut config = crate::config::load_config_from_path(new_root, &layout.config_path)?;
let mut config =
tracedecay_configuration::load_config_from_path(new_root, &layout.config_path)?;
config.root_dir = root_dir;
crate::config::save_config_to_path(&layout.config_path, &config)?;
tracedecay_configuration::save_config_to_path(&layout.config_path, &config)?;
}
registry
.upsert_code_project(&candidate.project_id, new_root, None, None, None)
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ tracedecay-agent-hosts = { path = "../tracedecay-agent-hosts", version = "0.1.0"
tracedecay-application = { path = "../tracedecay-application", version = "0.1.0" }
tracedecay-automation-runtime = { path = "../tracedecay-automation-runtime", version = "0.1.0" }
tracedecay-api = { path = "../tracedecay-api", version = "0.1.0" }
tracedecay-configuration = { path = "../tracedecay-configuration", version = "0.1.0" }
tracedecay-contracts = { path = "../tracedecay-contracts", version = "0.1.0" }
tracedecay-daemon-control = { path = "../tracedecay-daemon-control", version = "0.1.0" }
tracedecay-daemon-identity = { path = "../tracedecay-daemon-identity", version = "0.1.0" }
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay-cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub(crate) async fn handle_bench(
max_nodes: usize,
) -> tracedecay_domain::errors::Result<()> {
let resolved =
super::scope::resolve_project_scope(tracedecay::config::resolve_path(path)).await?;
super::scope::resolve_project_scope(tracedecay_configuration::resolve_path(path)).await?;
let queries_toml = queries
.map(std::fs::read_to_string)
.transpose()
Expand Down
Loading
Loading