use anyhow::{anyhow, bail, Context, Result};
use chrono::Local;
use colored::Colorize;
use std::path::Path;
use crate::followups::{self, FuStatus};
use crate::utils;
pub fn run(path: &str, fu_id: &str, title: Option<&str>) -> Result<()> {
let resolved = utils::resolve_project_root(path)
.ok_or_else(|| anyhow!("StrayMark not installed. Run 'straymark init' first."))?;
let project_root = &resolved.path;
let straymark_dir = project_root.join(".straymark");
let registry_path = followups::registry_path(project_root);
if !registry_path.exists() {
bail!(
"No follow-ups registry at {}.\n hint: see STRAYMARK.md §16 for the adoption walkthrough.",
registry_path.display()
);
}
let registry = followups::parse_registry(®istry_path)?;
let entry = followups::find_entry(®istry, fu_id)
.ok_or_else(|| {
anyhow!(
"Entry {} not found in the registry.\n hint: run `straymark followups list` to see entries.",
fu_id
)
})?
.clone();
if entry.status == FuStatus::Promoted {
bail!(
"{} is already promoted (Promoted to: {}). Nothing to do.",
entry.fu_id,
entry.promoted_to.as_deref().unwrap_or("?")
);
}
if matches!(entry.status, FuStatus::Closed | FuStatus::Superseded) {
bail!(
"{} is {} — promote applies to open/in-progress entries. Reopen it first if the debt is real.",
entry.fu_id,
entry.status.as_str()
);
}
let tde_title = title.unwrap_or(&entry.description).to_string();
let (tde_id, tde_path) = create_tde(project_root, &straymark_dir, &tde_title, &entry)?;
let mut body = followups::set_entry_field(®istry.body, &entry, "Status", "promoted");
let reparsed = followups::parse_registry_str(
®istry_path,
&followups::assemble(®istry.frontmatter_raw, &body),
)?;
let entry2 = followups::find_entry(&reparsed, &entry.fu_id).unwrap().clone();
body = followups::set_entry_field(&body, &entry2, "Destination", &tde_id);
let reparsed = followups::parse_registry_str(
®istry_path,
&followups::assemble(®istry.frontmatter_raw, &body),
)?;
let entry3 = followups::find_entry(&reparsed, &entry.fu_id).unwrap().clone();
body = followups::set_entry_field(&body, &entry3, "Promoted to", &tde_id);
let final_registry = followups::parse_registry_str(
®istry_path,
&followups::assemble(®istry.frontmatter_raw, &body),
)?;
let counters = followups::compute_counters(&final_registry);
let fm = followups::fm_apply_counters_and_v1(®istry.frontmatter_raw, &counters);
std::fs::write(®istry_path, followups::assemble(&fm, &body))?;
let tde_rel = tde_path
.strip_prefix(project_root)
.unwrap_or(&tde_path)
.display()
.to_string();
utils::success(&format!("{} promoted → {}", entry.fu_id, tde_id));
println!(" TDE created: {}", tde_rel);
println!(
" Registry updated: Status promoted, Destination/Promoted to → {} (counters recomputed).",
tde_id
);
println!();
println!(" {}", "Next steps:".bold());
println!(" 1. Fill impact / effort / type and the body sections in the TDE.");
println!(" 2. Prioritization and assignment stay human (AGENT-RULES.md §3).");
println!(
" 3. Commit both files together: {}",
format!("git add {} .straymark/follow-ups-backlog.md", tde_rel).dimmed()
);
println!();
Ok(())
}
fn create_tde(
project_root: &Path,
straymark_dir: &Path,
title: &str,
entry: &followups::Entry,
) -> Result<(String, std::path::PathBuf)> {
let lang = crate::config::StrayMarkConfig::resolve_language(project_root);
let templates_dir = straymark_dir.join("templates");
let template_path = utils::resolve_localized_path(&templates_dir, "TEMPLATE-TDE.md", &lang);
let template = std::fs::read_to_string(&template_path).with_context(|| {
format!(
"TDE template not found at {}. Run `straymark repair` to restore framework files.",
template_path.display()
)
})?;
let today = Local::now().format("%Y-%m-%d").to_string();
let tde_dir = straymark_dir.join("06-evolution").join("technical-debt");
let seq = next_tde_sequence(&tde_dir, &today);
let tde_id = format!("TDE-{}-{}", today, seq);
let mut content = template
.replace("id: TDE-YYYY-MM-DD-NNN", &format!("id: {}", tde_id))
.replace("YYYY-MM-DD-NNN", &format!("{}-{}", today, seq))
.replace("YYYY-MM-DD", &today)
.replace("[Technical debt title]", title)
.replace("[Título de la deuda técnica]", title)
.replace("[Technical Debt Title]", title)
.replace("[agent-name-v1.0]", "straymark-cli-promote")
.replace("[nombre-agente-v1.0]", "straymark-cli-promote");
if let Some(idx) = content.find("promoted_from_followup:") {
let line_end = content[idx..]
.find('\n')
.map(|i| idx + i)
.unwrap_or(content.len());
content.replace_range(
idx..line_end,
&format!("promoted_from_followup: {}", entry.fu_id),
);
} else if let Some(idx) = content.find("\n---\n") {
content.insert_str(idx, &format!("\npromoted_from_followup: {}", entry.fu_id));
}
let origin_note = format!(
"{} (origin: {})",
entry.description,
entry.origin.as_deref().unwrap_or("follow-ups backlog")
);
content = content
.replace(
"[Brief description of the identified technical debt]",
&origin_note,
)
.replace(
"[Descripción breve de la deuda técnica identificada]",
&origin_note,
);
let slug = slugify(title);
let filename = format!("TDE-{}-{}-{}.md", today, seq, slug);
utils::ensure_dir(&tde_dir)?;
let filepath = tde_dir.join(filename);
std::fs::write(&filepath, content)?;
Ok((tde_id, filepath))
}
fn next_tde_sequence(tde_dir: &Path, today: &str) -> String {
let prefix = format!("TDE-{}-", today);
let mut max_seq = 0u32;
if let Ok(entries) = std::fs::read_dir(tde_dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_str().unwrap_or("");
if let Some(rest) = name.strip_prefix(&prefix) {
let head: String = rest.chars().take(3).collect();
if head.chars().count() == 3 {
if let Ok(n) = head.parse::<u32>() {
max_seq = max_seq.max(n);
}
}
}
}
}
format!("{:03}", max_seq + 1)
}
fn slugify(title: &str) -> String {
let lower = title.to_lowercase();
let parts: Vec<&str> = lower
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|s| !s.is_empty())
.collect();
let slug = parts.join("-");
if slug.chars().count() > 50 {
let truncated: String = slug.chars().take(50).collect();
truncated.trim_end_matches('-').to_string()
} else {
slug
}
}