use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{bail, Context, Result};
use serde::Serialize;
use tracing::debug;
use crate::memory::entries::StructuredMemoryEntry;
use crate::memory::file::{append_block_to_contents, render_entry_block, rewrite_entry_blocks};
use crate::memory::{governance, queue};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::protected_write::ExclusiveWriteOptions;
use crate::state::runtime::{self as runtime_state, RuntimeMemoryEntry};
use crate::state::session;
use crate::timestamps;
const PROMOTABLE_TYPES: &[&str] = &["rule", "constraint", "heuristic", "attempt"];
pub(crate) fn is_promotable_entry_type(entry_type: &str) -> bool {
PROMOTABLE_TYPES.contains(&entry_type)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PromoteDestination {
BranchMemory,
RepoMemory,
ProfileMemory,
ProjectTruth { target_file: PathBuf },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SourceOutcome {
Active,
Superseded,
LinkOnly,
}
impl SourceOutcome {
fn action(self) -> &'static str {
match self {
Self::Active => "active",
Self::Superseded => "superseded",
Self::LinkOnly => "link_only",
}
}
fn resulting_source_state(self, current_state: &str) -> String {
match self {
Self::Active => "active".to_owned(),
Self::Superseded => "superseded".to_owned(),
Self::LinkOnly => current_state.to_owned(),
}
}
}
#[derive(Serialize)]
pub struct MemoryPromoteReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
project_id: String,
locality_id: String,
mode: &'static str,
source: MemorySurfaceView,
destination: MemorySurfaceView,
selected_entry: StructuredMemoryEntry,
destination_preview: DestinationPreviewView,
source_outcome: SourceOutcomeView,
governance: governance::GovernanceDecisionView,
#[serde(skip_serializing_if = "Option::is_none")]
write_result: Option<WriteResultView>,
#[serde(skip_serializing_if = "Option::is_none")]
staged_write: Option<queue::StagedMemoryOpView>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
warnings: Vec<String>,
}
#[derive(Serialize)]
struct MemorySurfaceView {
scope: &'static str,
path: String,
status: &'static str,
}
#[derive(Serialize)]
struct DestinationPreviewView {
append_mode: &'static str,
file_status: &'static str,
provenance_link: String,
entry_block: String,
note: String,
}
#[derive(Serialize)]
struct SourceOutcomeView {
action: &'static str,
current_state: String,
resulting_state: String,
linkage_target: String,
note: &'static str,
}
#[derive(Serialize)]
struct WriteResultView {
destination_action: &'static str,
destination_path: String,
source_action: &'static str,
source_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
written_destination_entry: Option<StructuredMemoryEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
written_source_entry: Option<StructuredMemoryEntry>,
}
impl CommandReport for MemoryPromoteReport {
fn exit_code(&self) -> ExitCode {
if self.ok {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn render_text(&self) {
if !self.ok {
println!(
"{}",
self.message
.as_deref()
.unwrap_or("Queued memory promotion was rejected.")
);
return;
}
let source_scope = self.source.scope.replace('_', " ");
let destination_scope = self.destination.scope.replace('_', " ");
match &self.write_result {
Some(_) => println!(
"Applied memory promotion for entry {}.",
self.selected_entry.id
),
None => println!(
"Prepared memory promotion preview for entry {}.",
self.selected_entry.id
),
}
println!("Mode: {}", self.mode);
println!(
"Source: {} ({}, {})",
self.source.path, self.source.status, source_scope
);
println!(
"Destination: {} ({}, {})",
self.destination.path, self.destination.status, destination_scope
);
println!("Entry type: {}", self.selected_entry.entry_type);
println!("Current state: {}", self.selected_entry.state);
println!(
"Source outcome: {} ({})",
self.source_outcome.action, self.source_outcome.note
);
println!(
"Destination provenance: {}",
self.destination_preview.provenance_link
);
println!(
"Governance: {} ({})",
self.governance.action.as_str(),
self.governance.status.as_str()
);
if let Some(write_result) = &self.write_result {
println!(
"Destination write: {} ({})",
write_result.destination_action, write_result.destination_path
);
println!(
"Source write: {} ({})",
write_result.source_action, write_result.source_path
);
}
if let Some(staged_write) = &self.staged_write {
println!("Queued op: {}", staged_write.op_id);
}
for warning in &self.warnings {
println!("Warning: {warning}");
}
println!();
println!("Destination block:");
println!("{}", self.destination_preview.entry_block);
println!();
println!("{}", self.destination_preview.note);
}
}
#[allow(clippy::too_many_arguments)]
pub fn run(
repo_root: &Path,
explicit_profile: Option<&str>,
entry_id: &str,
write: bool,
requested_source_outcome: Option<SourceOutcome>,
destination: PromoteDestination,
write_options: ExclusiveWriteOptions,
op_id_hint: Option<&str>,
) -> Result<MemoryPromoteReport> {
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_root, profile.clone())?;
ensure_profile_exists(&layout)?;
let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
anyhow::anyhow!(
"repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
repo_root.join(repo_marker::MARKER_FILE).display(),
repo_root.display(),
repo_root.display()
)
})?;
let locality_id = marker.locality_id;
ensure_repo_registry_exists(&layout, repo_root, &locality_id)?;
ensure_repo_overlay_exists(&layout, &locality_id)?;
let staged_request = if should_stage_via_queue(write, &destination, &layout)? {
let request_key = PromoteQueueRequestKey {
command: "memory-promote",
entry_id,
destination: destination_queue_label(&destination),
source_outcome: requested_source_outcome.map(SourceOutcome::action),
};
let request_fingerprint = queue::fingerprint(&request_key)?;
let op_id = queue::stable_op_id("promote", &request_fingerprint, op_id_hint);
Some(QueuedWriteRequest {
request_fingerprint,
op_id,
})
} else {
None
};
match destination {
PromoteDestination::BranchMemory => {
if runtime_state::resolve_active_branch_memory_path(repo_root, &layout, &locality_id)?
.is_none()
{
bail!(
"work-stream memory is only available on active non-trunk named branches; switch to a feature branch before using `ccd memory promote --destination work-stream-memory`"
);
}
let clone_memory = runtime_state::load_clone_memory_surface(&layout)?;
let (selected_runtime_entry, selected_entry) = load_selected_entry(
entry_id,
&clone_memory,
"workspace-memory",
"`ccd memory promote`",
)?;
validate_promotable_entry(&selected_entry)?;
run_branch_memory(
repo_root,
&layout,
&locality_id,
&clone_memory,
profile.as_str(),
&selected_entry,
&selected_runtime_entry,
write,
requested_source_outcome,
None,
&write_options,
)
}
PromoteDestination::RepoMemory => {
let (source_memory, selected_runtime_entry, selected_entry, source_scope, mode_prefix) =
load_repo_promotion_source(repo_root, &layout, &locality_id, entry_id)?;
validate_promotable_entry(&selected_entry)?;
run_repo_memory(
repo_root,
&layout,
&locality_id,
&source_memory,
source_scope,
mode_prefix,
profile.as_str(),
&selected_entry,
&selected_runtime_entry,
write,
requested_source_outcome,
staged_request.as_ref(),
&write_options,
)
}
PromoteDestination::ProfileMemory | PromoteDestination::ProjectTruth { .. } => {
let repo_memory = runtime_state::load_locality_memory_surface(&layout, &locality_id)?;
let (selected_runtime_entry, selected_entry) = load_selected_entry(
entry_id,
&repo_memory,
"project-memory",
"`ccd memory promote`",
)?;
validate_promotable_entry(&selected_entry)?;
match destination {
PromoteDestination::ProfileMemory => run_profile_memory(
repo_root,
&layout,
&locality_id,
&repo_memory,
profile.as_str(),
&selected_entry,
&selected_runtime_entry,
write,
requested_source_outcome,
staged_request.as_ref(),
&write_options,
),
PromoteDestination::ProjectTruth { ref target_file } => run_project_truth(
repo_root,
&layout,
&locality_id,
&repo_memory,
profile.as_str(),
&selected_entry,
&selected_runtime_entry,
write,
requested_source_outcome,
target_file,
None,
&write_options,
),
PromoteDestination::BranchMemory | PromoteDestination::RepoMemory => {
unreachable!("handled above")
}
}
}
}
}
#[derive(Clone, Debug)]
struct QueuedWriteRequest {
request_fingerprint: String,
op_id: String,
}
#[derive(Serialize)]
struct PromoteQueueRequestKey<'a> {
command: &'static str,
entry_id: &'a str,
destination: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
source_outcome: Option<&'static str>,
}
pub(crate) fn load_selected_entry(
entry_id: &str,
memory: &runtime_state::LoadedRuntimeMemorySurface,
scope_label: &str,
command_name: &str,
) -> Result<(RuntimeMemoryEntry, StructuredMemoryEntry)> {
if !memory
.entries
.iter()
.any(|entry| entry.as_structured_entry().is_some())
{
bail!(
"no structured {} entries are available at {}; add a `ccd-memory` block before running {}",
scope_label,
memory.source.path.display(),
command_name
);
}
let selected_runtime_entry = memory
.entries
.iter()
.find(|entry| entry.structured_id() == Some(entry_id))
.cloned()
.ok_or_else(|| {
let available_ids = memory
.entries
.iter()
.filter_map(RuntimeMemoryEntry::structured_id)
.collect::<Vec<_>>()
.join(", ");
anyhow::anyhow!(
"structured {} entry `{}` was not found in {}; available ids: {}",
scope_label,
entry_id,
memory.source.path.display(),
available_ids
)
})?;
let selected_entry = selected_runtime_entry
.as_structured_entry()
.expect("structured memory entry");
Ok((selected_runtime_entry, selected_entry))
}
fn validate_promotable_entry(selected_entry: &StructuredMemoryEntry) -> Result<()> {
if !is_promotable_entry_type(&selected_entry.entry_type) {
bail!(
"entry `{}` has type `{}`, which is viewable but not promotable in the first cut",
selected_entry.id,
selected_entry.entry_type
);
}
Ok(())
}
fn load_repo_promotion_source(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
entry_id: &str,
) -> Result<(
runtime_state::LoadedRuntimeMemorySurface,
RuntimeMemoryEntry,
StructuredMemoryEntry,
&'static str,
&'static str,
)> {
if runtime_state::resolve_active_branch_memory_path(repo_root, layout, locality_id)?.is_some() {
let branch_memory =
runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)?;
if let Some(selected_runtime_entry) = branch_memory
.entries
.iter()
.find(|entry| entry.structured_id() == Some(entry_id))
.cloned()
{
let selected_entry = selected_runtime_entry
.as_structured_entry()
.expect("branch memory structured entry");
return Ok((
branch_memory,
selected_runtime_entry,
selected_entry,
"work_stream_memory",
"work_stream_memory_to_project",
));
}
}
let clone_memory = runtime_state::load_clone_memory_surface(layout)?;
let (selected_runtime_entry, selected_entry) = load_selected_entry(
entry_id,
&clone_memory,
"workspace-memory",
"`ccd memory promote`",
)?;
Ok((
clone_memory,
selected_runtime_entry,
selected_entry,
"workspace_memory",
"workspace_memory_to_project",
))
}
#[allow(clippy::too_many_arguments)]
fn run_profile_memory(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
repo_memory: &runtime_state::LoadedRuntimeMemorySurface,
profile: &str,
selected_entry: &StructuredMemoryEntry,
selected_runtime_entry: &RuntimeMemoryEntry,
write: bool,
requested_source_outcome: Option<SourceOutcome>,
staged_request: Option<&QueuedWriteRequest>,
write_options: &ExclusiveWriteOptions,
) -> Result<MemoryPromoteReport> {
let profile_memory = runtime_state::load_profile_memory_surface(layout)?;
if profile_memory
.entries
.iter()
.any(|entry| entry.structured_id() == Some(selected_entry.id.as_str()))
{
bail!(
"profile memory already contains entry id `{}` at {}; refusing to write or preview an unexpected overwrite",
selected_entry.id,
profile_memory.source.path.display()
);
}
debug!(destination = "profile", path = %profile_memory.source.path.display(), write, "promoting memory entry");
let source_outcome = resolve_source_outcome(write, requested_source_outcome)?;
let preserves_existing_provenance = preserves_existing_source_provenance(selected_entry);
if write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
bail!(
"entry `{}` already carries `source_ref`; refusing to apply a write that would discard or overwrite existing provenance in the first write slice",
selected_entry.id
);
}
let autonomous_session_id = current_autonomous_session_id(layout)?;
let autonomous = autonomous_session_id.is_some();
let mut governance = if selected_entry.entry_type == "attempt" {
governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::Escalate,
status: governance::GovernanceStatus::Escalated,
target_scope: "profile_memory".to_owned(),
source_scope: Some("project_memory".to_owned()),
entry_type: selected_entry.entry_type.clone(),
rationale: "profile promotion needs stronger cross-project evidence than an `attempt` entry currently carries".to_owned(),
evidence_summary: vec![
"attempt entries default to work-stream or project scope".to_owned(),
"profile memory should only absorb broader lessons with stronger reuse evidence".to_owned(),
],
pressure_signals: vec![governance::signal(
"entry_type_scope_mismatch",
"attempt entries do not qualify for autonomous profile promotion by default",
)],
duplicate_digest: Some(governance::normalized_duplicate_digest(selected_entry)),
rate_limit: None,
},
)
} else {
build_promote_governance(PromoteGovernanceContext {
layout,
source_scope: "project_memory",
target_scope: "profile_memory",
selected_entry,
destination_entries: &profile_memory.entries,
autonomous,
session_id: autonomous_session_id.as_deref(),
current_op_id: staged_request.map(|request| request.op_id.as_str()),
action: governance::GovernanceAction::StageProfilePromotion,
rationale:
"profile-scope mutation stays on the governed promotion path until broader reuse is proven",
evidence_summary: vec![
format!("entry type `{}` is eligible for broad promotion", selected_entry.entry_type),
"profile memory mutations remain reversible and reviewable".to_owned(),
],
pressure_signals: Vec::new(),
})?
};
let provenance_link = format!(
"{}#{}",
repo_memory.source.path.display(),
selected_entry.id
);
let linkage_target = format!(
"{}#{}",
profile_memory.source.path.display(),
selected_entry.id
);
let source_linkage_target = source_linkage_target(
selected_entry,
&linkage_target,
preserves_existing_provenance,
);
let written_destination_runtime =
promoted_destination_entry(selected_runtime_entry, &provenance_link);
let written_source_runtime = linked_source_entry(
selected_runtime_entry,
source_outcome,
source_linkage_target.clone(),
)?;
let written_destination_entry = written_destination_runtime
.as_structured_entry()
.expect("promoted destination entry");
let written_source_entry = written_source_runtime
.as_structured_entry()
.expect("linked source entry");
let destination_preview = DestinationPreviewView {
append_mode: "append_block",
file_status: profile_memory.source.status.as_str(),
provenance_link: provenance_link.clone(),
entry_block: render_entry_block(&written_destination_entry),
note: promote_note(
write,
autonomous,
&governance,
"Applied with explicit approval. The block above was appended to profile memory.",
"Preview only. No files were changed. Rerun with `--write --source-outcome <value>` to append this block and update the source entry.",
),
};
let mut warnings = Vec::new();
if !write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
warnings.push(
"selected entry already carries `source_ref`; the current write path will refuse it until multi-hop provenance is modeled explicitly"
.to_owned(),
);
}
let next_profile_contents = write.then(|| {
append_block_to_contents(
&profile_memory.source.content,
&destination_preview.entry_block,
)
});
let next_repo_contents = if write {
Some(rewrite_entry_blocks(
&repo_memory.source.content,
&HashMap::from([(
selected_entry.id.clone(),
render_entry_block(&written_source_entry),
)]),
)?)
} else {
None
};
let write_result = write.then(|| WriteResultView {
destination_action: "append_block",
destination_path: profile_memory.source.path.display().to_string(),
source_action: "update_selected_block",
source_path: repo_memory.source.path.display().to_string(),
written_destination_entry: Some(written_destination_entry.clone()),
written_source_entry: Some(written_source_entry.clone()),
});
let report = MemoryPromoteReport {
command: "memory-promote",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_owned(),
project_id: locality_id.to_owned(),
locality_id: locality_id.to_owned(),
mode: if write {
"project_memory_to_profile_write"
} else {
"project_memory_to_profile_preview"
},
source: MemorySurfaceView {
scope: "project_memory",
path: repo_memory.source.path.display().to_string(),
status: repo_memory.source.status.as_str(),
},
destination: MemorySurfaceView {
scope: "profile_memory",
path: profile_memory.source.path.display().to_string(),
status: profile_memory.source.status.as_str(),
},
selected_entry: selected_entry.clone(),
destination_preview,
source_outcome: SourceOutcomeView {
action: source_outcome.action(),
current_state: selected_entry.state.clone(),
resulting_state: source_outcome.resulting_source_state(&selected_entry.state),
linkage_target: source_linkage_target,
note: source_outcome_note(
source_outcome,
write,
requested_source_outcome.is_some(),
"profile",
preserves_existing_provenance,
),
},
governance: governance.clone(),
write_result,
staged_write: None,
message: None,
warnings,
};
let mut report = report;
if write {
let mutation_id = staged_request
.map(|request| request.op_id.as_str())
.unwrap_or("memory-promote-direct");
governance = governance.with_rollback(
mutation_id,
write_options.actor_id.as_deref(),
write_options
.session_id
.as_deref()
.or(autonomous_session_id.as_deref()),
vec![selected_entry.id.clone()],
&profile_memory.source.content,
Some(&repo_memory.source.content),
);
report.governance = governance.clone();
}
if write && governance.blocked_for_write(autonomous) {
report.ok = false;
report.write_result = None;
report.message = Some(governance.blocked_write_message());
return Ok(report);
}
finalize_promote_report(
repo_root,
layout,
report,
write.then(|| queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: "profile_memory".to_owned(),
path: profile_memory.source.path.display().to_string(),
next_contents: next_profile_contents.expect("write checked"),
},
source: Some(queue::MemoryFileMutation {
scope: "project_memory".to_owned(),
path: repo_memory.source.path.display().to_string(),
next_contents: next_repo_contents.expect("write checked"),
}),
refresh_locality_id: Some(locality_id.to_owned()),
authored_entry_ids: vec![selected_entry.id.clone()],
governance: Some(governance),
}),
staged_request,
write_options,
)
}
#[allow(clippy::too_many_arguments)]
fn run_repo_memory(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
source_memory: &runtime_state::LoadedRuntimeMemorySurface,
source_scope: &'static str,
mode_prefix: &'static str,
profile: &str,
selected_entry: &StructuredMemoryEntry,
selected_runtime_entry: &RuntimeMemoryEntry,
write: bool,
requested_source_outcome: Option<SourceOutcome>,
staged_request: Option<&QueuedWriteRequest>,
write_options: &ExclusiveWriteOptions,
) -> Result<MemoryPromoteReport> {
let repo_memory = runtime_state::load_locality_memory_surface(layout, locality_id)?;
if repo_memory
.entries
.iter()
.any(|entry| entry.structured_id() == Some(selected_entry.id.as_str()))
{
bail!(
"project memory already contains entry id `{}` at {}; refusing to write or preview an unexpected overwrite",
selected_entry.id,
repo_memory.source.path.display()
);
}
debug!(destination = "project", path = %repo_memory.source.path.display(), write, "promoting memory entry");
let source_outcome = resolve_source_outcome(write, requested_source_outcome)?;
let preserves_existing_provenance = preserves_existing_source_provenance(selected_entry);
if write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
bail!(
"entry `{}` already carries `source_ref`; refusing to apply a write that would discard or overwrite existing provenance in the first write slice",
selected_entry.id
);
}
let autonomous_session_id = current_autonomous_session_id(layout)?;
let autonomous = autonomous_session_id.is_some();
let mut governance = build_promote_governance(PromoteGovernanceContext {
layout,
source_scope,
target_scope: "project_memory",
selected_entry,
destination_entries: &repo_memory.entries,
autonomous,
session_id: autonomous_session_id.as_deref(),
current_op_id: staged_request.map(|request| request.op_id.as_str()),
action: governance::GovernanceAction::StageProjectPromotion,
rationale: "project-scope mutation stays staged as a governed promotion candidate",
evidence_summary: vec![
format!(
"entry type `{}` is eligible for project promotion",
selected_entry.entry_type
),
format!("source scope `{source_scope}` was validated"),
],
pressure_signals: Vec::new(),
})?;
let provenance_link = format!(
"{}#{}",
source_memory.source.path.display(),
selected_entry.id
);
let linkage_target = format!(
"{}#{}",
repo_memory.source.path.display(),
selected_entry.id
);
let source_linkage_target = source_linkage_target(
selected_entry,
&linkage_target,
preserves_existing_provenance,
);
let written_destination_runtime =
promoted_destination_entry(selected_runtime_entry, &provenance_link);
let written_source_runtime = linked_source_entry(
selected_runtime_entry,
source_outcome,
source_linkage_target.clone(),
)?;
let written_destination_entry = written_destination_runtime
.as_structured_entry()
.expect("promoted destination entry");
let written_source_entry = written_source_runtime
.as_structured_entry()
.expect("linked source entry");
let destination_preview = DestinationPreviewView {
append_mode: "append_block",
file_status: repo_memory.source.status.as_str(),
provenance_link: provenance_link.clone(),
entry_block: render_entry_block(&written_destination_entry),
note: promote_note(
write,
autonomous,
&governance,
"Applied with explicit approval. The block above was appended to project memory.",
"Preview only. No files were changed. Rerun with `--write --source-outcome <value>` to append this block and update the source entry.",
),
};
let mut warnings = Vec::new();
if !write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
warnings.push(
"selected entry already carries `source_ref`; the current write path will refuse it until multi-hop provenance is modeled explicitly"
.to_owned(),
);
}
let next_repo_contents = write.then(|| {
append_block_to_contents(
&repo_memory.source.content,
&destination_preview.entry_block,
)
});
let next_source_contents = if write {
Some(rewrite_entry_blocks(
&source_memory.source.content,
&HashMap::from([(
selected_entry.id.clone(),
render_entry_block(&written_source_entry),
)]),
)?)
} else {
None
};
let write_result = write.then(|| WriteResultView {
destination_action: "append_block",
destination_path: repo_memory.source.path.display().to_string(),
source_action: "update_selected_block",
source_path: source_memory.source.path.display().to_string(),
written_destination_entry: Some(written_destination_entry.clone()),
written_source_entry: Some(written_source_entry.clone()),
});
let report = MemoryPromoteReport {
command: "memory-promote",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_owned(),
project_id: locality_id.to_owned(),
locality_id: locality_id.to_owned(),
mode: match (write, mode_prefix) {
(true, "work_stream_memory_to_project") => "work_stream_memory_to_project_write",
(true, _) => "workspace_memory_to_project_write",
(false, "work_stream_memory_to_project") => "work_stream_memory_to_project_preview",
(false, _) => "workspace_memory_to_project_preview",
},
source: MemorySurfaceView {
scope: source_scope,
path: source_memory.source.path.display().to_string(),
status: source_memory.source.status.as_str(),
},
destination: MemorySurfaceView {
scope: "project_memory",
path: repo_memory.source.path.display().to_string(),
status: repo_memory.source.status.as_str(),
},
selected_entry: selected_entry.clone(),
destination_preview,
source_outcome: SourceOutcomeView {
action: source_outcome.action(),
current_state: selected_entry.state.clone(),
resulting_state: source_outcome.resulting_source_state(&selected_entry.state),
linkage_target: source_linkage_target,
note: source_outcome_note(
source_outcome,
write,
requested_source_outcome.is_some(),
"project",
preserves_existing_provenance,
),
},
governance: governance.clone(),
write_result,
staged_write: None,
message: None,
warnings,
};
let mut report = report;
if write {
let mutation_id = staged_request
.map(|request| request.op_id.as_str())
.unwrap_or("memory-promote-direct");
governance = governance.with_rollback(
mutation_id,
write_options.actor_id.as_deref(),
write_options
.session_id
.as_deref()
.or(autonomous_session_id.as_deref()),
vec![selected_entry.id.clone()],
&repo_memory.source.content,
Some(&source_memory.source.content),
);
report.governance = governance.clone();
}
if write && governance.blocked_for_write(autonomous) {
report.ok = false;
report.write_result = None;
report.message = Some(governance.blocked_write_message());
return Ok(report);
}
finalize_promote_report(
repo_root,
layout,
report,
write.then(|| queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: "project_memory".to_owned(),
path: repo_memory.source.path.display().to_string(),
next_contents: next_repo_contents.expect("write checked"),
},
source: Some(queue::MemoryFileMutation {
scope: source_scope.to_owned(),
path: source_memory.source.path.display().to_string(),
next_contents: next_source_contents.expect("write checked"),
}),
refresh_locality_id: Some(locality_id.to_owned()),
authored_entry_ids: vec![selected_entry.id.clone()],
governance: Some(governance),
}),
staged_request,
write_options,
)
}
#[allow(clippy::too_many_arguments)]
fn run_branch_memory(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
clone_memory: &runtime_state::LoadedRuntimeMemorySurface,
profile: &str,
selected_entry: &StructuredMemoryEntry,
selected_runtime_entry: &RuntimeMemoryEntry,
write: bool,
requested_source_outcome: Option<SourceOutcome>,
staged_request: Option<&QueuedWriteRequest>,
write_options: &ExclusiveWriteOptions,
) -> Result<MemoryPromoteReport> {
let branch_memory = runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)?;
if branch_memory
.entries
.iter()
.any(|entry| entry.structured_id() == Some(selected_entry.id.as_str()))
{
bail!(
"work-stream memory already contains entry id `{}` at {}; refusing to write or preview an unexpected overwrite",
selected_entry.id,
branch_memory.source.path.display()
);
}
debug!(destination = "work_stream", path = %branch_memory.source.path.display(), write, "promoting memory entry");
let source_outcome = resolve_source_outcome(write, requested_source_outcome)?;
let preserves_existing_provenance = preserves_existing_source_provenance(selected_entry);
if write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
bail!(
"entry `{}` already carries `source_ref`; refusing to apply a write that would discard or overwrite existing provenance in the first write slice",
selected_entry.id
);
}
let autonomous_session_id = current_autonomous_session_id(layout)?;
let autonomous = autonomous_session_id.is_some();
let repo_memory = runtime_state::load_locality_memory_surface(layout, locality_id)?;
let mut governance = if let Some(duplicate) =
governance::find_runtime_duplicate(&repo_memory.entries, selected_entry)
{
governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::Discard,
status: governance::GovernanceStatus::Discarded,
target_scope: "work_stream_memory".to_owned(),
source_scope: Some("workspace_memory".to_owned()),
entry_type: selected_entry.entry_type.clone(),
rationale: format!(
"work-stream capture overlaps existing project memory entry `{}`",
duplicate.entry_id
),
evidence_summary: vec![
"work-stream admission stays local only when project memory does not already contain the lesson"
.to_owned(),
],
pressure_signals: vec![governance::signal(
"broader_scope_overlap",
format!(
"project_memory already carries normalized duplicate `{}`",
duplicate.entry_id
),
)],
duplicate_digest: Some(duplicate.digest),
rate_limit: None,
},
)
} else {
build_promote_governance(PromoteGovernanceContext {
layout,
source_scope: "workspace_memory",
target_scope: "work_stream_memory",
selected_entry,
destination_entries: &branch_memory.entries,
autonomous,
session_id: autonomous_session_id.as_deref(),
current_op_id: staged_request.map(|request| request.op_id.as_str()),
action: governance::GovernanceAction::AdmitWorkStream,
rationale:
"work-stream admission is allowed for focused lessons on an active non-trunk work stream",
evidence_summary: vec![
format!("entry type `{}` is eligible for work-stream admission", selected_entry.entry_type),
"no project-memory overlap was detected".to_owned(),
],
pressure_signals: Vec::new(),
})?
};
let provenance_link = format!(
"{}#{}",
clone_memory.source.path.display(),
selected_entry.id
);
let linkage_target = format!(
"{}#{}",
branch_memory.source.path.display(),
selected_entry.id
);
let source_linkage_target = source_linkage_target(
selected_entry,
&linkage_target,
preserves_existing_provenance,
);
let written_destination_runtime =
promoted_destination_entry(selected_runtime_entry, &provenance_link);
let written_source_runtime = linked_source_entry(
selected_runtime_entry,
source_outcome,
source_linkage_target.clone(),
)?;
let written_destination_entry = written_destination_runtime
.as_structured_entry()
.expect("promoted destination entry");
let written_source_entry = written_source_runtime
.as_structured_entry()
.expect("linked source entry");
let destination_preview = DestinationPreviewView {
append_mode: "append_block",
file_status: branch_memory.source.status.as_str(),
provenance_link: provenance_link.clone(),
entry_block: render_entry_block(&written_destination_entry),
note: promote_note(
write,
autonomous,
&governance,
"Applied with explicit approval. The block above was appended to work-stream memory.",
"Preview only. No files were changed. Rerun with `--write --source-outcome <value>` to append this block and update the source entry.",
),
};
let mut warnings = Vec::new();
if !write && selected_entry.source_ref.is_some() && !preserves_existing_provenance {
warnings.push(
"selected entry already carries `source_ref`; the current write path will refuse it until multi-hop provenance is modeled explicitly"
.to_owned(),
);
}
let next_branch_contents = write.then(|| {
append_block_to_contents(
&branch_memory.source.content,
&destination_preview.entry_block,
)
});
let next_clone_contents = if write {
Some(rewrite_entry_blocks(
&clone_memory.source.content,
&HashMap::from([(
selected_entry.id.clone(),
render_entry_block(&written_source_entry),
)]),
)?)
} else {
None
};
let write_result = write.then(|| WriteResultView {
destination_action: "append_block",
destination_path: branch_memory.source.path.display().to_string(),
source_action: "update_selected_block",
source_path: clone_memory.source.path.display().to_string(),
written_destination_entry: Some(written_destination_entry.clone()),
written_source_entry: Some(written_source_entry.clone()),
});
let report = MemoryPromoteReport {
command: "memory-promote",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_owned(),
project_id: locality_id.to_owned(),
locality_id: locality_id.to_owned(),
mode: if write {
"workspace_memory_to_work_stream_write"
} else {
"workspace_memory_to_work_stream_preview"
},
source: MemorySurfaceView {
scope: "workspace_memory",
path: clone_memory.source.path.display().to_string(),
status: clone_memory.source.status.as_str(),
},
destination: MemorySurfaceView {
scope: "work_stream_memory",
path: branch_memory.source.path.display().to_string(),
status: branch_memory.source.status.as_str(),
},
selected_entry: selected_entry.clone(),
destination_preview,
source_outcome: SourceOutcomeView {
action: source_outcome.action(),
current_state: selected_entry.state.clone(),
resulting_state: source_outcome.resulting_source_state(&selected_entry.state),
linkage_target: source_linkage_target,
note: source_outcome_note(
source_outcome,
write,
requested_source_outcome.is_some(),
"work stream",
preserves_existing_provenance,
),
},
governance: governance.clone(),
write_result,
staged_write: None,
message: None,
warnings,
};
let mut report = report;
if write {
let mutation_id = staged_request
.map(|request| request.op_id.as_str())
.unwrap_or("memory-promote-direct");
governance = governance.with_rollback(
mutation_id,
write_options.actor_id.as_deref(),
write_options
.session_id
.as_deref()
.or(autonomous_session_id.as_deref()),
vec![selected_entry.id.clone()],
&branch_memory.source.content,
Some(&clone_memory.source.content),
);
report.governance = governance.clone();
}
if write && governance.blocked_for_write(autonomous) {
report.ok = false;
report.write_result = None;
report.message = Some(governance.blocked_write_message());
return Ok(report);
}
finalize_promote_report(
repo_root,
layout,
report,
write.then(|| queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: "work_stream_memory".to_owned(),
path: branch_memory.source.path.display().to_string(),
next_contents: next_branch_contents.expect("write checked"),
},
source: Some(queue::MemoryFileMutation {
scope: "workspace_memory".to_owned(),
path: clone_memory.source.path.display().to_string(),
next_contents: next_clone_contents.expect("write checked"),
}),
refresh_locality_id: Some(locality_id.to_owned()),
authored_entry_ids: vec![selected_entry.id.clone()],
governance: Some(governance),
}),
staged_request,
write_options,
)
}
#[allow(clippy::too_many_arguments)]
fn run_project_truth(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
repo_memory: &runtime_state::LoadedRuntimeMemorySurface,
profile: &str,
selected_entry: &StructuredMemoryEntry,
selected_runtime_entry: &RuntimeMemoryEntry,
write: bool,
requested_source_outcome: Option<SourceOutcome>,
target_file: &Path,
staged_request: Option<&QueuedWriteRequest>,
write_options: &ExclusiveWriteOptions,
) -> Result<MemoryPromoteReport> {
if requested_source_outcome.is_some() {
bail!(
"`--source-outcome` is not applicable for `--destination project-truth`; the source entry is always linked without a state change"
);
}
debug!(destination = "project_truth", path = %target_file.display(), write, "promoting memory entry");
let autonomous_session_id = current_autonomous_session_id(layout)?;
let autonomous = autonomous_session_id.is_some();
let provenance_link = format!(
"{}#{}",
repo_memory.source.path.display(),
selected_entry.id
);
let linkage_target = format!("{}#{}", target_file.display(), selected_entry.id);
let preserves_existing_provenance = preserves_existing_source_provenance(selected_entry);
let source_linkage_target = source_linkage_target(
selected_entry,
&linkage_target,
preserves_existing_provenance,
);
let mut governance = governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::DraftProjectTruth,
status: governance::GovernanceStatus::Allowed,
target_scope: "project_truth".to_owned(),
source_scope: Some("project_memory".to_owned()),
entry_type: selected_entry.entry_type.clone(),
rationale: "project-truth promotion only drafts a reviewable section and never directly widens canonical truth without normal project workflow".to_owned(),
evidence_summary: vec![
"project-truth output remains a draft or recommendation".to_owned(),
"the source memory entry is only linked back to the draft".to_owned(),
],
pressure_signals: Vec::new(),
duplicate_digest: Some(governance::normalized_duplicate_digest(selected_entry)),
rate_limit: None,
},
);
let draft_section = project_truth_draft_section(selected_entry, &provenance_link);
let file_status = if target_file.exists() {
"loaded"
} else {
"missing"
};
let destination_preview = DestinationPreviewView {
append_mode: "append_plain_section",
file_status,
provenance_link: provenance_link.clone(),
entry_block: draft_section.clone(),
note: promote_note(
write,
autonomous,
&governance,
"Applied with explicit approval. The draft section above was appended to the target file.",
"Preview only. No files were changed. Rerun with `--write` to append the draft section to the target file.",
),
};
let source_outcome = SourceOutcome::LinkOnly;
let mut canonical_destination_path = None;
let mut current_target_contents = None;
let next_target_contents = if write {
let canonical_repo = fs::canonicalize(repo_root)
.with_context(|| format!("cannot resolve repo root `{}`", repo_root.display()))?;
let canonical_file = fs::canonicalize(target_file).with_context(|| {
format!(
"target file `{}` does not exist or cannot be resolved; create it before running with `--write`",
target_file.display()
)
})?;
if !canonical_file.starts_with(&canonical_repo) {
bail!(
"target file `{}` is outside the repo root `{}`; refusing to write outside the repo boundary",
target_file.display(),
repo_root.display()
);
}
let current_content = fs::read_to_string(&canonical_file)
.with_context(|| format!("cannot read target file `{}`", canonical_file.display()))?;
let draft_marker = format!("<!-- ccd-memory-draft: {} ", selected_entry.id);
if current_content.contains(&draft_marker) {
bail!(
"target file `{}` already contains a draft for entry `{}`; remove the existing draft before re-promoting",
canonical_file.display(),
selected_entry.id
);
}
let next_content = append_plain_section(¤t_content, &draft_section);
current_target_contents = Some(current_content);
canonical_destination_path = Some(canonical_file);
Some(next_content)
} else {
None
};
let next_repo_contents = if write {
let canonical_file = canonical_destination_path
.as_ref()
.expect("canonical target path when write is enabled");
let canonical_linkage_target =
format!("{}#{}", canonical_file.display(), selected_entry.id);
let canonical_source_linkage_target = if preserves_existing_provenance {
selected_entry
.source_ref
.clone()
.expect("existing source_ref when preserving provenance")
} else {
canonical_linkage_target
};
let canonical_source_runtime = linked_source_entry(
selected_runtime_entry,
source_outcome,
canonical_source_linkage_target,
)?;
let canonical_source_entry = canonical_source_runtime
.as_structured_entry()
.expect("canonical source entry");
Some((
rewrite_entry_blocks(
&repo_memory.source.content,
&HashMap::from([(
selected_entry.id.clone(),
render_entry_block(&canonical_source_entry),
)]),
)?,
canonical_source_entry,
))
} else {
None
};
let write_result = if write {
let canonical_file = canonical_destination_path
.as_ref()
.expect("canonical target path when write is enabled");
let canonical_source_entry = next_repo_contents
.as_ref()
.map(|(_, entry)| entry.clone())
.expect("canonical source entry when write is enabled");
Some(WriteResultView {
destination_action: "append_plain_section",
destination_path: canonical_file.display().to_string(),
source_action: "update_selected_block",
source_path: repo_memory.source.path.display().to_string(),
written_destination_entry: None,
written_source_entry: Some(canonical_source_entry),
})
} else {
None
};
let report = MemoryPromoteReport {
command: "memory-promote",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_owned(),
project_id: locality_id.to_owned(),
locality_id: locality_id.to_owned(),
mode: if write {
"project_memory_to_project_truth_write"
} else {
"project_memory_to_project_truth_preview"
},
source: MemorySurfaceView {
scope: "project_memory",
path: repo_memory.source.path.display().to_string(),
status: repo_memory.source.status.as_str(),
},
destination: MemorySurfaceView {
scope: "project_truth",
path: target_file.display().to_string(),
status: file_status,
},
selected_entry: selected_entry.clone(),
destination_preview,
source_outcome: SourceOutcomeView {
action: source_outcome.action(),
current_state: selected_entry.state.clone(),
resulting_state: source_outcome.resulting_source_state(&selected_entry.state),
linkage_target: source_linkage_target,
note: source_outcome_note(
source_outcome,
write,
false,
"project_truth",
preserves_existing_provenance,
),
},
governance: governance.clone(),
write_result,
staged_write: None,
message: None,
warnings: Vec::new(),
};
let mut report = report;
if write {
let mutation_id = staged_request
.map(|request| request.op_id.as_str())
.unwrap_or("memory-promote-direct");
governance = governance.with_rollback(
mutation_id,
write_options.actor_id.as_deref(),
write_options
.session_id
.as_deref()
.or(autonomous_session_id.as_deref()),
vec![selected_entry.id.clone()],
current_target_contents.as_deref().unwrap_or(""),
Some(&repo_memory.source.content),
);
report.governance = governance.clone();
}
finalize_promote_report(
repo_root,
layout,
report,
write.then(|| queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: "project_truth".to_owned(),
path: canonical_destination_path
.as_ref()
.expect("canonical target path when write is enabled")
.display()
.to_string(),
next_contents: next_target_contents.expect("write checked"),
},
source: Some(queue::MemoryFileMutation {
scope: "project_memory".to_owned(),
path: repo_memory.source.path.display().to_string(),
next_contents: next_repo_contents
.as_ref()
.map(|(contents, _)| contents.clone())
.expect("write checked"),
}),
refresh_locality_id: Some(locality_id.to_owned()),
authored_entry_ids: vec![selected_entry.id.clone()],
governance: Some(governance),
}),
staged_request,
write_options,
)
}
fn should_stage_via_queue(
write: bool,
_destination: &PromoteDestination,
layout: &StateLayout,
) -> Result<bool> {
if !write {
return Ok(false);
}
queue::queue_required_for_autonomous(layout)
}
fn destination_queue_label(destination: &PromoteDestination) -> &'static str {
match destination {
PromoteDestination::BranchMemory => "work_stream_memory",
PromoteDestination::RepoMemory => "project_memory",
PromoteDestination::ProfileMemory => "profile_memory",
PromoteDestination::ProjectTruth { .. } => "project_truth",
}
}
pub(crate) fn replay_request(
entry_id: &str,
requested_source_outcome: Option<SourceOutcome>,
destination: &PromoteDestination,
op_id_hint: Option<&str>,
) -> Result<(String, String)> {
let request_key = PromoteQueueRequestKey {
command: "memory-promote",
entry_id,
destination: destination_queue_label(destination),
source_outcome: requested_source_outcome.map(SourceOutcome::action),
};
let request_fingerprint = queue::fingerprint(&request_key)?;
let op_id = queue::stable_op_id("promote", &request_fingerprint, op_id_hint);
Ok((request_fingerprint, op_id))
}
fn finalize_promote_report(
repo_root: &Path,
layout: &StateLayout,
mut report: MemoryPromoteReport,
plan: Option<queue::MemoryOpPlan>,
staged_request: Option<&QueuedWriteRequest>,
write_options: &ExclusiveWriteOptions,
) -> Result<MemoryPromoteReport> {
let Some(plan) = plan else {
return Ok(report);
};
if let Some(staged_request) = staged_request {
let staged_write = queue::stage_and_reconcile(
repo_root,
layout,
"memory-promote",
&staged_request.op_id,
&staged_request.request_fingerprint,
write_options,
&plan,
)?;
let staged_ok = matches!(
staged_write.outcome,
crate::state::protected_write::AppendWriteOutcome::Applied
| crate::state::protected_write::AppendWriteOutcome::IdempotentNoop
);
if !staged_ok {
report.ok = false;
report.write_result = None;
report.message = staged_write.message.clone();
}
report.staged_write = Some(staged_write);
if report.ok {
queue::persist_report_snapshot(layout, &staged_request.op_id, &report)?;
}
return Ok(report);
}
let outcome = queue::apply_direct_plan(repo_root, layout, &plan)?;
if outcome == crate::state::protected_write::AppendWriteOutcome::IdempotentNoop {
if let Some(write_result) = report.write_result.as_mut() {
write_result.destination_action = "already_applied";
write_result.source_action = "already_applied";
}
}
Ok(report)
}
struct PromoteGovernanceContext<'a> {
layout: &'a StateLayout,
source_scope: &'a str,
target_scope: &'a str,
selected_entry: &'a StructuredMemoryEntry,
destination_entries: &'a [RuntimeMemoryEntry],
autonomous: bool,
session_id: Option<&'a str>,
current_op_id: Option<&'a str>,
action: governance::GovernanceAction,
rationale: &'a str,
evidence_summary: Vec<String>,
pressure_signals: Vec<governance::GovernancePressureSignalView>,
}
fn build_promote_governance(
mut context: PromoteGovernanceContext<'_>,
) -> Result<governance::GovernanceDecisionView> {
if let Some(duplicate) =
governance::find_runtime_duplicate(context.destination_entries, context.selected_entry)
{
return Ok(governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::Discard,
status: governance::GovernanceStatus::Discarded,
target_scope: context.target_scope.to_owned(),
source_scope: Some(context.source_scope.to_owned()),
entry_type: context.selected_entry.entry_type.clone(),
rationale: format!(
"a normalized duplicate already exists in {} as `{}`",
context.target_scope, duplicate.entry_id
),
evidence_summary: vec![
"duplicate suppression runs before any broader-scope mutation".to_owned(),
],
pressure_signals: vec![governance::signal(
"duplicate_match",
format!(
"{} already contains normalized duplicate `{}`",
context.target_scope, duplicate.entry_id
),
)],
duplicate_digest: Some(duplicate.digest),
rate_limit: None,
},
));
}
let rate_limit = if context.autonomous {
governance::session_rate_limit(
context.layout,
context.session_id,
context.target_scope,
context.current_op_id,
)?
} else {
None
};
if let Some(rate_limit) = &rate_limit {
if rate_limit.remaining == 0 {
context.pressure_signals.push(governance::signal(
"session_cap_reached",
format!(
"{} already has {} queued admissions or promotions in this session",
rate_limit.scope, rate_limit.count
),
));
} else if rate_limit.remaining == 1 {
context.pressure_signals.push(governance::signal(
"session_cap_near",
format!(
"{} has one remaining governed mutation slot in this session",
rate_limit.scope
),
));
}
}
Ok(governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: context.action,
status: if rate_limit
.as_ref()
.is_some_and(|limit| limit.status == "reached")
{
governance::GovernanceStatus::RateLimited
} else {
governance::GovernanceStatus::Allowed
},
target_scope: context.target_scope.to_owned(),
source_scope: Some(context.source_scope.to_owned()),
entry_type: context.selected_entry.entry_type.clone(),
rationale: context.rationale.to_owned(),
evidence_summary: context.evidence_summary,
pressure_signals: context.pressure_signals,
duplicate_digest: Some(governance::normalized_duplicate_digest(
context.selected_entry,
)),
rate_limit,
},
))
}
fn promote_note(
write: bool,
autonomous: bool,
governance: &governance::GovernanceDecisionView,
write_note: &str,
preview_note: &str,
) -> String {
if write && governance.blocked_for_write(autonomous) {
return format!(
"Write was not applied. Governance outcome: {} ({})",
governance.action.as_str(),
governance.blocked_write_message()
);
}
if !write && governance.status != governance::GovernanceStatus::Allowed {
return format!(
"Preview only. Governance outcome: {} ({})",
governance.action.as_str(),
governance.rationale
);
}
if write {
write_note.to_owned()
} else {
preview_note.to_owned()
}
}
fn current_autonomous_session_id(layout: &StateLayout) -> Result<Option<String>> {
if queue::queue_required_for_autonomous(layout)? {
session::load_session_id(layout)
} else {
Ok(None)
}
}
fn resolve_source_outcome(
write: bool,
requested_source_outcome: Option<SourceOutcome>,
) -> Result<SourceOutcome> {
match (write, requested_source_outcome) {
(true, None) => bail!(
"`ccd memory promote --write` requires `--source-outcome <active|superseded|link-only>` so the source transition stays explicit"
),
(_, Some(outcome)) => Ok(outcome),
(false, None) => Ok(SourceOutcome::LinkOnly),
}
}
fn promoted_destination_entry(
selected_entry: &RuntimeMemoryEntry,
provenance_link: &str,
) -> RuntimeMemoryEntry {
let decay_class = selected_entry
.structured_decay_class()
.map(str::to_owned)
.or(Some("stable".to_owned()));
selected_entry
.with_structured_state("active")
.with_source_ref(Some(provenance_link.to_owned()))
.with_decay_class(decay_class)
}
fn linked_source_entry(
selected_entry: &RuntimeMemoryEntry,
source_outcome: SourceOutcome,
next_source_ref: String,
) -> Result<RuntimeMemoryEntry> {
let current_state = selected_entry
.structured_state()
.expect("structured source entry");
let resulting_state = source_outcome.resulting_source_state(current_state);
let entry = selected_entry
.with_structured_state(&resulting_state)
.with_source_ref(Some(next_source_ref));
if current_state != "superseded" && resulting_state == "superseded" {
let superseded_at = Some(timestamps::current_utc_rfc3339()?);
return Ok(entry.with_superseded_at(superseded_at));
}
Ok(entry)
}
fn preserves_existing_source_provenance(selected_entry: &StructuredMemoryEntry) -> bool {
selected_entry.source_ref.is_some() && selected_entry.state == "promotion_candidate"
}
fn source_linkage_target(
selected_entry: &StructuredMemoryEntry,
linkage_target: &str,
preserves_existing_provenance: bool,
) -> String {
if preserves_existing_provenance {
selected_entry
.source_ref
.clone()
.expect("existing source_ref when preserving provenance")
} else {
linkage_target.to_owned()
}
}
fn source_outcome_note(
source_outcome: SourceOutcome,
write: bool,
explicitly_requested: bool,
destination_scope: &'static str,
preserves_existing_provenance: bool,
) -> &'static str {
if preserves_existing_provenance {
if write {
return match source_outcome {
SourceOutcome::Active => {
"source entry kept its existing provenance link and was marked active"
}
SourceOutcome::Superseded => {
"source entry kept its existing provenance link and was marked superseded"
}
SourceOutcome::LinkOnly => {
"source entry kept its existing provenance link without changing its state"
}
};
}
if explicitly_requested {
return "previewing the requested source outcome; existing source provenance would be preserved on write";
}
return "preview defaults to `link_only`; existing source provenance would be preserved on write";
}
if write {
return match source_outcome {
SourceOutcome::Active => {
match destination_scope {
"branch" => {
"source entry was linked to the promoted branch entry and marked active"
}
"repo" => {
"source entry was linked to the promoted repo entry and marked active"
}
"project_truth" => {
"source entry was linked to the project-truth draft and marked active"
}
_ => "source entry was linked to the promoted profile entry and marked active",
}
}
SourceOutcome::Superseded => {
match destination_scope {
"branch" => {
"source entry was linked to the promoted branch entry and marked superseded"
}
"repo" => {
"source entry was linked to the promoted repo entry and marked superseded"
}
"project_truth" => {
"source entry was linked to the project-truth draft and marked superseded"
}
_ => {
"source entry was linked to the promoted profile entry and marked superseded"
}
}
}
SourceOutcome::LinkOnly => {
match destination_scope {
"branch" => {
"source entry was linked to the promoted branch entry without changing its state"
}
"repo" => {
"source entry was linked to the promoted repo entry without changing its state"
}
"project_truth" => {
"source entry was linked to the project-truth draft without changing its state"
}
_ => {
"source entry was linked to the promoted profile entry without changing its state"
}
}
}
};
}
if explicitly_requested {
return "previewing the requested source outcome; rerun with `--write` to apply it";
}
if destination_scope == "project_truth" {
return "preview defaults to `link_only` for project-truth destination";
}
"preview defaults to `link_only` until an explicit write is approved"
}
fn project_truth_draft_section(entry: &StructuredMemoryEntry, provenance_link: &str) -> String {
format!(
"<!-- ccd-memory-draft: {} | type: {} | source: {} -->\n{}",
entry.id, entry.entry_type, provenance_link, entry.content
)
}
fn append_plain_section(contents: &str, section: &str) -> String {
if contents.trim().is_empty() {
return format!("{section}\n");
}
format!("{}\n\n{section}\n", contents.trim_end_matches(['\n', '\r']))
}
fn ensure_profile_exists(layout: &StateLayout) -> Result<()> {
let profile_root = layout.profile_root();
if profile_root.is_dir() {
return Ok(());
}
bail!(
"profile `{}` does not exist at {}; bootstrap it with `ccd attach` before using `ccd memory promote`",
layout.profile(),
profile_root.display()
)
}
fn ensure_repo_registry_exists(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
) -> Result<()> {
let registry_path = layout.repo_metadata_path(locality_id)?;
match repo_registry::load(®istry_path)? {
Some(_) => Ok(()),
None => bail!(
"repo registry is missing for locality_id `{}` at {}; run `ccd link --path {}` or `ccd attach --path {}` again",
locality_id,
registry_path.display(),
repo_root.display(),
repo_root.display()
),
}
}
fn ensure_repo_overlay_exists(layout: &StateLayout, locality_id: &str) -> Result<()> {
let overlay_root = layout.repo_overlay_root(locality_id)?;
if overlay_root.is_dir() {
return Ok(());
}
bail!(
"repo overlay is missing for locality_id `{}` at {}; run `ccd link --path .` or `ccd attach --path .` again",
locality_id,
overlay_root.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn promotable_entry_types_include_the_lint_surface() {
assert!(is_promotable_entry_type("rule"));
assert!(is_promotable_entry_type("constraint"));
assert!(is_promotable_entry_type("heuristic"));
assert!(is_promotable_entry_type("attempt"));
assert!(!is_promotable_entry_type("observation"));
}
}