use std::path::Path;
use zenith_core::{KdlAdapter, KdlSource, Severity, validate};
use zenith_session::adapter::OsFs;
use zenith_session::{CandidateStatus, StorePaths, get_scratch_snapshot, list_scratch};
use zenith_tx::{merge_candidate_page, reconcile_candidate_tokens};
use crate::commands::workspace::scratch::open_store;
use crate::history::{Recorded, record_edit_in};
pub fn promote(
doc_path: &Path,
cand_id: &str,
into_page: &str,
id_suffix: &str,
) -> Result<String, String> {
let paths = open_store()?;
promote_in(&paths, doc_path, cand_id, into_page, id_suffix)
}
pub fn promote_in(
paths: &StorePaths,
doc_path: &Path,
cand_id: &str,
into_page: &str,
id_suffix: &str,
) -> Result<String, String> {
let fs = OsFs;
let main_bytes = std::fs::read(doc_path)
.map_err(|e| format!("cannot read '{}': {e}", doc_path.display()))?;
let mut main_doc = KdlAdapter
.parse(main_bytes.as_slice())
.map_err(|e| format!("cannot parse '{}': {}", doc_path.display(), e.message))?;
let doc_id = main_doc.doc_id.clone().ok_or_else(|| {
format!(
"'{}' has no history yet (no doc-id); edit it with `zenith tx --apply` or \
`zenith library add` first",
doc_path.display()
)
})?;
let entries = list_scratch(&fs, paths, &doc_id).map_err(|e| e.message)?;
let entry = entries
.iter()
.find(|e| e.id == cand_id)
.ok_or_else(|| format!("candidate not found: {cand_id}"))?;
if entry.status != CandidateStatus::Selected {
let actual = match entry.status {
CandidateStatus::Draft => "draft",
CandidateStatus::Selected => "selected",
CandidateStatus::Rejected => "rejected",
};
return Err(format!(
"candidate {cand_id} must have status \"selected\" to promote, but its status is \"{actual}\"; \
use `zenith workspace candidate` to mark it selected first"
));
}
let snap_bytes = get_scratch_snapshot(&fs, paths, &doc_id, entry).map_err(|e| e.message)?;
let cand_doc = KdlAdapter.parse(snap_bytes.as_slice()).map_err(|e| {
format!(
"cannot parse candidate snapshot for {cand_id}: {}",
e.message
)
})?;
let source_page = if entry.page_id == "*" {
cand_doc
.body
.pages
.first()
.ok_or_else(|| format!("candidate snapshot for {cand_id} has no pages"))?
} else {
cand_doc
.body
.pages
.iter()
.find(|p| p.id == entry.page_id)
.or_else(|| cand_doc.body.pages.first())
.ok_or_else(|| {
format!(
"candidate snapshot for {cand_id} has no page with id {:?} and no pages at all",
entry.page_id
)
})?
};
let target_page = main_doc
.body
.pages
.iter_mut()
.find(|p| p.id == into_page)
.ok_or_else(|| {
format!(
"target page {:?} not found in '{}'",
into_page,
doc_path.display()
)
})?;
merge_candidate_page(source_page, target_page, id_suffix);
reconcile_candidate_tokens(&cand_doc.tokens, &mut main_doc.tokens);
let report = validate(&main_doc);
let errors: Vec<&zenith_core::Diagnostic> = report
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
if !errors.is_empty() {
let msgs: Vec<String> = errors
.iter()
.map(|d| format!(" error[{}]: {}", d.code, d.message))
.collect();
return Err(format!(
"promote produced validation errors — document not written:\n{}",
msgs.join("\n")
));
}
let formatted = KdlAdapter
.format(&main_doc)
.map_err(|e| format!("format failed: {}", e.message))?;
let Recorded { bytes, warning, .. } =
record_edit_in(paths, &formatted, doc_path, "workspace.promote");
if let Some(w) = &warning {
eprintln!("warning: {w}");
}
std::fs::write(doc_path, &bytes)
.map_err(|e| format!("cannot write '{}': {e}", doc_path.display()))?;
Ok(format!("promoted {cand_id} → page {into_page}"))
}