use crate::domain::model::check::{CheckViolation, CheckViolationKind, EventLogIssue, Severity};
use crate::domain::model::entity_ref::EntityRef;
pub fn render_with_hint(violation: &CheckViolation) -> String {
let body = render(&violation.kind);
if matches!(
(&violation.kind, violation.severity),
(CheckViolationKind::ScanIssue { .. }, Severity::Error)
) {
format!(
"{body}\n hint: edit the file to repair the frontmatter; \
`cartu fmt` only handles syntactically-valid YAML."
)
} else {
body
}
}
pub fn render(kind: &CheckViolationKind) -> String {
match kind {
CheckViolationKind::WrongIdPrefix { id, expected } => {
format!("id '{id}' does not start with configured prefix '{expected}'")
}
CheckViolationKind::DuplicateId { id, also_at } => {
let paths = also_at
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ");
format!("duplicate id {id}: also found at {paths}")
}
CheckViolationKind::IdSlugMismatch {
id_suffix,
dir_prefix,
} => format!("id suffix ({id_suffix}) does not match directory prefix ({dir_prefix})"),
CheckViolationKind::LinkTargetNotFound { target } => {
format!("link target '{target}' not found in workspace")
}
CheckViolationKind::EventLogBroken { cause } => render_event_log_issue(cause),
CheckViolationKind::TagDescriptorViolation { owner, cause } => {
format!("{owner}: {}", cause.message())
}
CheckViolationKind::MissingBackPointer {
source,
forward,
back,
} => {
format!("missing back-pointer '{back}' → {source} (reciprocal of '{forward}')")
}
CheckViolationKind::PrematureBackPointer {
source,
source_status,
back,
} => format!(
"premature back-pointer '{back}' → {source}: {source} is still '{source_status}'"
),
CheckViolationKind::MissingForwardLink {
source,
forward,
back,
} => {
format!("missing forward link '{forward}' → {source} (reciprocal of '{back}')")
}
CheckViolationKind::TagOrderViolated {
tag_key,
parent,
parent_value,
child,
child_value,
} => format!(
"ordered rule violated for '{tag_key}': parent {parent} ({tag_key}={parent_value}) \
must rank above child {child} ({tag_key}={child_value})"
),
CheckViolationKind::MultipleParents { child, parents } => format!(
"{child} has multiple parents ({}); a child has at most one parent",
join_refs(parents)
),
CheckViolationKind::ParentOfCycle { cycle } => {
format!("cycle detected: {}", join_refs_with(cycle, " → "))
}
CheckViolationKind::TerminalParentWithOpenChildren {
parent,
parent_status,
open_children,
} => format!(
"{parent} is {parent_status} but has open children ({}); reopen the parent or close / re-parent the children",
join_refs(open_children)
),
CheckViolationKind::ScanIssue { detail } => detail.clone(),
CheckViolationKind::BrokenCompanionLink { from, to } => {
format!("broken link in {from}: '{to}' does not exist")
}
}
}
fn join_refs(refs: &[EntityRef]) -> String {
join_refs_with(refs, ", ")
}
fn join_refs_with(refs: &[EntityRef], sep: &str) -> String {
refs.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.join(sep)
}
fn render_event_log_issue(issue: &EventLogIssue) -> String {
match issue {
EventLogIssue::CreatedStatusUnknown { status } => {
format!("'created' event status '{status}' is not a known status")
}
EventLogIssue::CreatedStatusMismatch { status, expected } => format!(
"'created' event status '{status}' does not match the configured initial status '{expected}'"
),
EventLogIssue::FirstActionNotCreated { found } => {
format!("first event action must be 'created', got '{found}'")
}
EventLogIssue::StatusChangeFromUnknown { from } => {
format!("'from' value '{from}' is not a known status")
}
EventLogIssue::StatusChangeToUnknown { to } => {
format!("'to' value '{to}' is not a known status")
}
EventLogIssue::EventChainBroken { from, prev_status } => format!(
"event chain broken: 'from' is '{from}' but previous status was '{prev_status}'"
),
EventLogIssue::InvalidTransition { from, to } => {
format!("invalid transition '{from}' → '{to}'")
}
EventLogIssue::TerminalStatusOutbound { from } => {
format!("status '{from}' is terminal — no outgoing transitions are allowed")
}
EventLogIssue::FinalStatusMismatch {
last_to,
current_status,
} => format!(
"last 'status_changed' to is '{last_to}' but current status is '{current_status}'"
),
}
}