use anyhow::{anyhow, Result};
use chrono::Utc;
use dialoguer::{theme::ColorfulTheme, Input};
use std::path::PathBuf;
use crate::cli::SplitArgs;
use crate::model::{Requirement, Status};
use crate::storage::{self, load_for_mutation};
use crate::validate;
pub fn run(mut args: SplitArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
args.id = super::resolve_id(&project, &args.id)?;
let original = project
.requirements
.get(&args.id)
.ok_or_else(|| anyhow!("no such requirement: {}", args.id))?
.clone();
if matches!(original.status, Status::Obsolete) {
return Err(anyhow!(
"{} is already obsolete; nothing to split.",
args.id
));
}
let statements = if !args.into.is_empty() {
args.into.clone()
} else {
prompt_for_parts(&original)?
};
if statements.len() < 2 {
return Err(anyhow!(
"split needs at least 2 parts (got {}); for a rewrite use `req update --statement`",
statements.len()
));
}
let now = Utc::now();
let mut staged: Vec<Requirement> = Vec::new();
for (i, stmt) in statements.iter().enumerate() {
let part = Requirement {
id: String::new(),
title: synth_title(&original.title, i, statements.len()),
statement: stmt.trim().to_string(),
rationale: format!(
"Split from {} ({} of {}). Original rationale: {}",
args.id,
i + 1,
statements.len(),
original.rationale
),
acceptance: original.acceptance.clone(),
kind: original.kind,
priority: original.priority,
status: Status::Draft,
tags: original.tags.clone(),
links: Vec::new(),
created: now,
updated: now,
history: vec![super::history(
format!("split from {}", args.id),
args.reason.clone(),
)],
tests: Vec::new(),
};
let findings = validate::validate_requirement(&part);
let errs = validate::errors_only(&findings);
if !errs.is_empty() {
let msg: Vec<String> = errs
.iter()
.map(|f| format!("[{}] {}", f.field, f.message))
.collect();
return Err(anyhow!(
"part #{} failed validation; nothing mutated. {}",
i + 1,
msg.join("; ")
));
}
for f in findings.iter().filter(|f| !f.error) {
eprintln!(" WARN part #{} [{}] {}", i + 1, f.field, f.message);
}
staged.push(part);
}
let mut child_ids: Vec<String> = Vec::new();
for mut part in staged {
let new_id = project.allocate_id();
part.id = new_id.clone();
project.requirements.insert(new_id.clone(), part);
child_ids.push(new_id);
}
if !args.keep_original {
let o = project.requirements.get_mut(&args.id).unwrap();
o.status = Status::Obsolete;
o.updated = now;
o.history.push(super::history(
format!("split into {}", child_ids.join(", ")),
args.reason.clone(),
));
} else {
let o = project.requirements.get_mut(&args.id).unwrap();
o.history.push(super::history(
format!(
"split — created sibling parts {} (original kept active)",
child_ids.join(", ")
),
args.reason.clone(),
));
o.updated = now;
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"original": args.id,
"retired": !args.keep_original,
"parts": child_ids,
}))?
);
} else if args.keep_original {
println!(
"Created {} part(s) from {} (original kept active): {}",
child_ids.len(),
args.id,
child_ids.join(", ")
);
} else {
println!(
"Retired {} and created {} part(s): {}",
args.id,
child_ids.len(),
child_ids.join(", ")
);
}
Ok(())
}
fn prompt_for_parts(original: &Requirement) -> Result<Vec<String>> {
let theme = ColorfulTheme::default();
println!("Splitting {} — {}", original.id, original.title);
println!();
println!("Original statement:");
println!(" {}", original.statement);
println!();
println!("Enter each new atomic statement on its own line. Empty line ends input.");
let mut out: Vec<String> = Vec::new();
loop {
let label = format!("Part {} statement", out.len() + 1);
let line: String = Input::with_theme(&theme)
.with_prompt(label)
.allow_empty(true)
.interact_text()?;
if line.trim().is_empty() {
break;
}
out.push(line);
}
Ok(out)
}
fn synth_title(parent: &str, idx: usize, total: usize) -> String {
let suffix = format!(" — part {} of {}", idx + 1, total);
let max_parent = 120usize.saturating_sub(suffix.chars().count());
let mut t: String = parent.chars().take(max_parent).collect();
t.push_str(&suffix);
t
}