use crate::RelationExplanation;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CoordinateOrigin {
method: Option<String>,
rationale: Option<String>,
motivation: Option<String>,
}
impl CoordinateOrigin {
pub fn from_relation_explanation(explanation: &RelationExplanation) -> Self {
let Some(method) = normalize(explanation.method()) else {
return Self::default();
};
Self {
method: Some(method),
rationale: normalize(explanation.rationale()),
motivation: normalize(explanation.motivation()),
}
}
pub fn method(&self) -> Option<&str> {
self.method.as_deref()
}
pub fn rationale(&self) -> Option<&str> {
self.rationale.as_deref()
}
pub fn motivation(&self) -> Option<&str> {
self.motivation.as_deref()
}
pub fn is_write(&self) -> bool {
self.method.is_none()
}
}
fn normalize(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RelationSemanticClass;
#[test]
fn a_label_given_at_write_is_its_own_origin() {
let explanation = RelationExplanation::new(RelationSemanticClass::Structural)
.with_rationale("Memory scope contains this entry.");
let origin = CoordinateOrigin::from_relation_explanation(&explanation);
assert!(origin.is_write());
assert_eq!(origin.rationale(), None, "boilerplate is not an origin");
}
#[test]
fn a_label_stitched_on_later_carries_method_why_and_who() {
let explanation = RelationExplanation::new(RelationSemanticClass::Structural)
.with_method("kmp_relabel")
.with_rationale("The decision belongs to the issue it closed.")
.with_optional_motivation(Some(
"Relabelled by agent:claude at 2026-09-05T06:40:00Z.".into(),
));
let origin = CoordinateOrigin::from_relation_explanation(&explanation);
assert!(!origin.is_write());
assert_eq!(origin.method(), Some("kmp_relabel"));
assert_eq!(
origin.rationale(),
Some("The decision belongs to the issue it closed.")
);
assert!(
origin
.motivation()
.is_some_and(|note| note.starts_with("Relabelled by"))
);
}
}