1use crate::moves::{
20 Cited, Course, Kind, Move, MoveId, Moves, Says, Scope, Standing, Trouble, Undernath,
21};
22use serde::Serialize;
23use somatize_store::Store;
24use std::collections::{BTreeMap, BTreeSet, HashMap};
25
26#[derive(Debug, Clone, Serialize)]
28pub struct Seen {
29 pub name: String,
30 pub id: MoveId,
33 pub kind: Kind,
34 pub prose: String,
35 pub who: String,
36 pub when: u64,
37 pub under: Vec<String>,
38 pub about: Vec<String>,
42 pub scope: Vec<String>,
43 pub cites: Vec<Cited>,
44 pub course: Option<Course>,
46 pub standing: Option<Standing>,
49 pub pruned: bool,
51}
52
53#[derive(Debug, Clone, Serialize)]
55pub struct Told {
56 pub from: String,
57 pub says: Says,
58 pub to: String,
59 pub scope: Vec<String>,
60 pub partly: bool,
61 pub withdrawn: bool,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct Folded {
70 pub root: String,
72 pub by: String,
74 pub course: Course,
75 pub why: String,
78 pub hides: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize)]
84pub struct Reasoning {
85 pub tree: String,
86 pub moves: Vec<Seen>,
88 pub says: Vec<Told>,
89 pub folded: Vec<Folded>,
90}
91
92impl Reasoning {
93 pub fn went(&self, name: &str) -> Option<&Seen> {
95 self.moves.iter().find(|seen| seen.name == name)
96 }
97
98 pub fn below(&self, name: &str) -> Vec<&Seen> {
101 self.moves
102 .iter()
103 .filter(|other| {
104 other
105 .under
106 .iter()
107 .chain(&other.about)
108 .any(|one| one == name)
109 })
110 .collect()
111 }
112
113 pub fn covers(&self, by: &[String]) -> Result<Vec<String>, Trouble> {
121 let mut children: HashMap<&str, Vec<&str>> = HashMap::new();
122 for seen in &self.moves {
123 for parent in &seen.under {
124 children.entry(parent).or_default().push(&seen.name);
125 }
126 }
127 let mut reached: BTreeSet<&str> = BTreeSet::new();
128 let mut asking: Vec<&str> = Vec::new();
129 for name in by {
130 let seen = self
131 .went(name)
132 .ok_or_else(|| Trouble::NoSuchName { name: name.clone() })?;
133 asking.push(&seen.name);
134 }
135 while let Some(one) = asking.pop() {
136 if !reached.insert(one) {
137 continue;
138 }
139 asking.extend(children.get(one).into_iter().flatten().copied());
140 }
141 Ok(self
142 .moves
143 .iter()
144 .filter(|seen| reached.contains(seen.name.as_str()))
145 .map(|seen| seen.name.clone())
146 .collect())
147 }
148}
149
150pub fn reasoned(tree: &str, kept: &dyn Store) -> Result<Reasoning, Trouble> {
153 let moves = Moves::of(tree, kept);
154 let known = moves.all()?;
155 let under = moves.under()?;
156 let standing = moves.standing()?;
157 let courses = moves.courses()?;
158 let withdrawn = moves.withdrawn()?;
159 let named = |id: MoveId| known.get(&id).map(|body| body.name.clone());
160 let names = |ids: Vec<MoveId>| ids.into_iter().filter_map(named).collect();
164
165 let seen = known
166 .iter()
167 .map(|(id, body)| Seen {
168 name: body.name.clone(),
169 id: *id,
170 kind: body.kind,
171 prose: body.prose.clone(),
172 who: body.who.clone(),
173 when: body.when,
174 under: names(under.parents_of(*id)),
175 about: match body.kind == Kind::Decision && under.parents_of(*id).is_empty() {
176 true => names(body.scope.0.iter().copied().collect()),
177 false => Vec::new(),
178 },
179 scope: names(body.scope.0.iter().copied().collect()),
180 cites: body.cites.clone(),
181 course: body.course,
182 standing: standing.get(id).copied(),
183 pruned: matches!(
184 courses.get(id),
185 Some((_, Course::Abandon | Course::Superseded))
186 ),
187 })
188 .collect();
189
190 let mut said: Vec<(MoveId, MoveId, Told)> = moves
191 .says()?
192 .into_iter()
193 .filter_map(|one| {
194 Some((
195 one.from,
196 one.to,
197 Told {
198 from: named(one.from)?,
199 says: one.says,
200 to: named(one.to)?,
201 scope: names(one.scope.0.iter().copied().collect()),
202 partly: one.in_part,
203 withdrawn: withdrawn.contains(&one.from),
204 },
205 ))
206 })
207 .collect();
208 said.sort_by_key(|(from, to, told)| (*from, *to, told.says.as_str()));
209
210 Ok(Reasoning {
211 tree: tree.to_string(),
212 moves: seen,
213 says: said.into_iter().map(|(_, _, told)| told).collect(),
214 folded: folded(&known, &under, &courses),
215 })
216}
217
218fn folded(
224 known: &BTreeMap<MoveId, Move>,
225 under: &Undernath,
226 courses: &BTreeMap<MoveId, (MoveId, Course)>,
227) -> Vec<Folded> {
228 let mut rows = Vec::new();
229 for (id, body) in known {
230 let Some(course) = body.course else { continue };
231 if !matches!(course, Course::Abandon | Course::Superseded) {
232 continue;
233 }
234 for root in crate::moves::abandoning(*id, body, under).0 {
235 if courses.get(&root).map(|(by, _)| *by) != Some(*id) {
236 continue;
237 }
238 let Some(named) = known.get(&root) else {
239 continue;
240 };
241 let mut hides: Vec<MoveId> = Scope::of([root])
244 .covers(under)
245 .into_iter()
246 .filter(|one| {
247 matches!(
248 courses.get(one),
249 Some((_, Course::Abandon | Course::Superseded))
250 )
251 })
252 .collect();
253 hides.sort_unstable();
254 rows.push(Folded {
255 root: named.name.clone(),
256 by: body.name.clone(),
257 course,
258 why: body.prose.clone(),
259 hides: hides
260 .into_iter()
261 .filter_map(|one| known.get(&one).map(|body| body.name.clone()))
262 .collect(),
263 });
264 }
265 }
266 rows.sort_by(|a, b| a.root.cmp(&b.root));
267 rows
268}
269
270pub fn outlined(reasoning: &Reasoning, from: Option<&str>, all_lines: bool) -> Vec<String> {
276 let mut lines = Vec::new();
277 let hidden: BTreeMap<&str, &Folded> = match all_lines {
278 true => BTreeMap::new(),
279 false => reasoning
280 .folded
281 .iter()
282 .map(|one| (one.root.as_str(), one))
283 .collect(),
284 };
285 let roots: Vec<&Seen> = match from {
286 Some(name) => reasoning.moves.iter().filter(|s| s.name == name).collect(),
287 None => reasoning
290 .moves
291 .iter()
292 .filter(|s| s.under.is_empty() && s.about.is_empty())
293 .collect(),
294 };
295 let mut drawn: BTreeSet<&str> = BTreeSet::new();
296 for root in roots {
297 outline_from(reasoning, root, 0, &hidden, &mut drawn, &mut lines);
298 }
299 lines
300}
301
302const ENOUGH: usize = 64;
304
305fn outline_from<'a>(
306 reasoning: &'a Reasoning,
307 seen: &'a Seen,
308 depth: usize,
309 hidden: &BTreeMap<&str, &Folded>,
310 drawn: &mut BTreeSet<&'a str>,
311 lines: &mut Vec<String>,
312) {
313 let pad = " ".repeat(depth);
314 let again = !drawn.insert(seen.name.as_str());
317 let said = match (seen.standing, seen.course) {
318 (Some(standing), _) => format!(" · {standing}"),
319 (_, Some(course)) => format!(" · {course}"),
320 _ => String::new(),
321 };
322 lines.push(format!(
323 "{pad}{} · {}{said} · {}{}",
324 seen.name,
325 seen.kind,
326 shortened(&seen.prose),
327 match again {
328 true => " · (again)",
329 false => "",
330 }
331 ));
332 if again {
333 return;
334 }
335 if let Some(one) = hidden.get(seen.name.as_str()) {
336 lines.push(format!(
337 "{pad} ⋯ {} folded · {} · {}",
338 one.hides.len(),
339 one.course,
340 shortened(&one.why)
341 ));
342 return;
343 }
344 for child in reasoning.below(&seen.name) {
345 outline_from(reasoning, child, depth + 1, hidden, drawn, lines);
346 }
347}
348
349fn shortened(prose: &str) -> String {
351 let first = prose.lines().next().unwrap_or_default().trim();
352 match first.char_indices().nth(ENOUGH) {
353 Some((at, _)) => format!("{}…", &first[..at].trim_end()),
354 None => first.to_string(),
355 }
356}