Skip to main content

spool/
lifecycle_format.rs

1use crate::lifecycle_service::LifecycleAction;
2use crate::lifecycle_store::LedgerEntry;
3use crate::lifecycle_summary;
4
5pub fn state_label(entry: &LedgerEntry) -> &'static str {
6    match entry.record.state {
7        crate::domain::MemoryLifecycleState::Draft => "draft",
8        crate::domain::MemoryLifecycleState::Candidate => "candidate",
9        crate::domain::MemoryLifecycleState::Accepted => "accepted",
10        crate::domain::MemoryLifecycleState::Canonical => "canonical",
11        crate::domain::MemoryLifecycleState::Archived => "archived",
12    }
13}
14
15pub fn action_label(entry: &LedgerEntry) -> &'static str {
16    match entry.action {
17        crate::domain::MemoryLedgerAction::RecordManual => "record_manual",
18        crate::domain::MemoryLedgerAction::ProposeAi => "propose_ai",
19        crate::domain::MemoryLedgerAction::SubmitProposal => "submit_proposal",
20        crate::domain::MemoryLedgerAction::Accept => "accept",
21        crate::domain::MemoryLedgerAction::PromoteToCanonical => "promote_to_canonical",
22        crate::domain::MemoryLedgerAction::Archive => "archive",
23    }
24}
25
26pub fn metadata_lines(entry: &LedgerEntry) -> String {
27    let mut lines = String::new();
28    if let Some(actor) = entry.metadata.actor.as_deref() {
29        lines.push_str(&format!("- actor: {}\n", actor));
30    }
31    if let Some(reason) = entry.metadata.reason.as_deref() {
32        lines.push_str(&format!("- reason: {}\n", reason));
33    }
34    if !entry.metadata.evidence_refs.is_empty() {
35        lines.push_str(&format!(
36            "- evidence_refs: {}\n",
37            entry.metadata.evidence_refs.join(", ")
38        ));
39    }
40    lines
41}
42
43pub fn action_button_label(action: LifecycleAction) -> &'static str {
44    match action {
45        LifecycleAction::Accept => "Accept",
46        LifecycleAction::PromoteToCanonical => "Promote",
47        LifecycleAction::Archive => "Archive",
48    }
49}
50
51pub fn render_list_item(entry: &LedgerEntry, include_record_id: bool) -> String {
52    if include_record_id {
53        format!(
54            "- `{}` [{}] {} ({})",
55            entry.record_id,
56            state_label(entry),
57            entry.record.title,
58            entry.record.memory_type
59        )
60    } else {
61        format!(
62            "- [{}] {} ({})",
63            state_label(entry),
64            entry.record.title,
65            entry.record.memory_type
66        )
67    }
68}
69
70pub fn render_action_result(action: LifecycleAction, entry: &LedgerEntry) -> String {
71    lifecycle_summary::render_action_text(action, entry)
72}
73
74pub fn render_create_result(kind: &str, entry: &LedgerEntry) -> String {
75    lifecycle_summary::render_create_text(kind, entry)
76}
77
78pub fn render_gui_list_label(entry: &LedgerEntry) -> String {
79    format!(
80        "[{}] {} ({})",
81        state_label(entry),
82        entry.record.title,
83        entry.record.memory_type
84    )
85}
86
87pub fn render_detail(
88    entry: &LedgerEntry,
89    quote_record_id: bool,
90    include_summary_section: bool,
91) -> String {
92    let record_id = if quote_record_id {
93        format!("`{}`", entry.record_id)
94    } else {
95        entry.record_id.clone()
96    };
97
98    let mut lines = vec![
99        "# Memory record".to_string(),
100        String::new(),
101        format!("- record_id: {}", record_id),
102        format!("- state: {}", state_label(entry)),
103        format!("- action: {}", action_label(entry)),
104        format!("- title: {}", entry.record.title),
105        format!("- memory_type: {}", entry.record.memory_type),
106        format!("- scope: {:?}", entry.record.scope),
107        format!("- source_kind: {:?}", entry.source_kind),
108        format!("- scope_key: {}", entry.scope_key),
109    ];
110    if let Some(project_id) = entry.record.project_id.as_deref() {
111        lines.push(format!("- project_id: {}", project_id));
112    }
113    if let Some(user_id) = entry.record.user_id.as_deref() {
114        lines.push(format!("- user_id: {}", user_id));
115    }
116    if let Some(sensitivity) = entry.record.sensitivity.as_deref() {
117        lines.push(format!("- sensitivity: {}", sensitivity));
118    }
119    if !entry.record.entities.is_empty() {
120        lines.push(format!("- entities: {}", entry.record.entities.join(", ")));
121    }
122    if !entry.record.tags.is_empty() {
123        lines.push(format!("- tags: {}", entry.record.tags.join(", ")));
124    }
125    if !entry.record.triggers.is_empty() {
126        lines.push(format!("- triggers: {}", entry.record.triggers.join(", ")));
127    }
128    if !entry.record.related_files.is_empty() {
129        lines.push(format!(
130            "- related_files: {}",
131            entry.record.related_files.join(", ")
132        ));
133    }
134    if !entry.record.related_records.is_empty() {
135        lines.push(format!(
136            "- related_records: {}",
137            entry.record.related_records.join(", ")
138        ));
139    }
140    if !entry.record.applies_to.is_empty() {
141        lines.push(format!(
142            "- applies_to: {}",
143            entry.record.applies_to.join(", ")
144        ));
145    }
146    if let Some(ref supersedes) = entry.record.supersedes {
147        lines.push(format!("- supersedes: {}", supersedes));
148    }
149    if let Some(ref valid_until) = entry.record.valid_until {
150        lines.push(format!("- valid_until: {}", valid_until));
151    }
152    if include_summary_section {
153        lines.push(String::new());
154        lines.push("## Summary".to_string());
155        lines.push(String::new());
156        lines.push(entry.record.summary.clone());
157    } else {
158        lines.push(format!("- summary: {}", entry.record.summary));
159    }
160    let metadata = metadata_lines(entry);
161    if !metadata.is_empty() {
162        let insert_at = lines
163            .len()
164            .saturating_sub(if include_summary_section { 3 } else { 1 });
165        let metadata_lines: Vec<String> = metadata
166            .trim_end()
167            .lines()
168            .map(ToString::to_string)
169            .collect();
170        lines.splice(insert_at..insert_at, metadata_lines);
171    }
172    lines.join("\n")
173}
174
175pub fn render_history(record_id: &str, entries: &[LedgerEntry], quote_record_id: bool) -> String {
176    let record_id = if quote_record_id {
177        format!("`{record_id}`")
178    } else {
179        record_id.to_string()
180    };
181    if entries.is_empty() {
182        return format!("# Memory history\n\n- record_id: {record_id}\n- none");
183    }
184
185    let mut output = format!(
186        "# Memory history\n\n- record_id: {}\n- events: {}\n\n",
187        record_id,
188        entries.len()
189    );
190    for (index, entry) in entries.iter().enumerate() {
191        output.push_str(&format!(
192            "## {}. {}\n- recorded_at: {}\n- action: {}\n- state: {}\n- title: {}\n{}\n",
193            index + 1,
194            entry.record_id,
195            entry.recorded_at,
196            action_label(entry),
197            state_label(entry),
198            entry.record.title,
199            metadata_lines(entry)
200        ));
201    }
202    output.trim_end().to_string()
203}
204
205pub fn render_list(
206    title: &str,
207    entries: &[LedgerEntry],
208    include_record_id: bool,
209    markdown_heading: bool,
210) -> String {
211    if entries.is_empty() {
212        return if markdown_heading {
213            format!("# {title}\n\n- none")
214        } else {
215            format!("{title}\n\n- none")
216        };
217    }
218
219    let mut output = String::new();
220    if markdown_heading {
221        output.push_str(&format!("# {title}\n\n"));
222    } else {
223        output.push_str(title);
224        output.push_str("\n\n");
225    }
226    for entry in entries {
227        output.push_str(&render_list_item(entry, include_record_id));
228        output.push('\n');
229    }
230    output
231}