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}