Skip to main content

somatize_tree/
reasoning.rs

1//! The reasoning, read back: every move with what it stands at, and what folds.
2//!
3//! [`walk`](crate::walk) is the record read this way and the argument is the
4//! same one: what the terminal prints and what a notebook draws are the **same
5//! answer** read twice, so it is worked out once and here. A second copy in
6//! Python would be a view that quietly disagreed with the terminal about what
7//! an investigation contains.
8//!
9//! It answers in **names** and not in ids. A move carries a name because the
10//! store's slot stops identifying it the moment nobody is holding it in a
11//! variable — and reading it back is exactly that moment. The id is kept as a
12//! field, because it is what says in which order these were made.
13//!
14//! What is added here and is in no store: whether a move is on a line somebody
15//! abandoned. [`Moves::decided`] answers that for commits only, and an attempt
16//! nobody ever ran cites none — which is precisely the move a decision needs to
17//! be able to abandon.
18
19use 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/// One move, derived and in names.
27#[derive(Debug, Clone, Serialize)]
28pub struct Seen {
29    pub name: String,
30    /// The store's slot. Kept because it is what orders siblings by when they
31    /// were made, which no walk can recover.
32    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    /// Where it belongs on the page when nothing hangs it: a decision's scope
39    /// names what is abandoned, and a decision drawn floating beside the line
40    /// it ended is the one thing a reader cannot join up.
41    pub about: Vec<String>,
42    pub scope: Vec<String>,
43    pub cites: Vec<Cited>,
44    /// Only a decision carries one.
45    pub course: Option<Course>,
46    /// Only a question or a hypothesis. `None` and not `open`: an attempt is
47    /// not a question nobody has answered.
48    pub standing: Option<Standing>,
49    /// Whether something abandoned the line it is on. Derived, never stored.
50    pub pruned: bool,
51}
52
53/// One thing said from a move towards another, in names.
54#[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    /// Whether it no longer counts towards a standing, because the commit its
62    /// evidence came from was judged `invalid`. Still written and still drawn:
63    /// a standing that moved on its own has to say what moved it.
64    pub withdrawn: bool,
65}
66
67/// A line somebody abandoned: what it hides, and why, in words.
68#[derive(Debug, Clone, Serialize)]
69pub struct Folded {
70    /// The move the decision named. Everything under it is in `hides`.
71    pub root: String,
72    /// The decision that said so.
73    pub by: String,
74    pub course: Course,
75    /// The decision's own prose. **Pruning says why or it is deletion with a
76    /// nicer name.**
77    pub why: String,
78    /// What folds with it, in the order they were made, the root among them.
79    pub hides: Vec<String>,
80}
81
82/// A whole reasoning, ready to print or to draw.
83#[derive(Debug, Clone, Serialize)]
84pub struct Reasoning {
85    pub tree: String,
86    /// In the order they were made, which is the order siblings are drawn in.
87    pub moves: Vec<Seen>,
88    pub says: Vec<Told>,
89    pub folded: Vec<Folded>,
90}
91
92impl Reasoning {
93    /// The move of that name, if there is one.
94    pub fn went(&self, name: &str) -> Option<&Seen> {
95        self.moves.iter().find(|seen| seen.name == name)
96    }
97
98    /// What hangs under that move, in the order they were made — a decision
99    /// that named it and hangs nowhere included.
100    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    /// What a scope with those roots reaches: the roots and everything under
114    /// them, in the order they were made.
115    ///
116    /// The one derivation a reader cannot redo by hand and get right — `under`
117    /// is multivalued, so it is a walk over a DAG and not a subtree. With it,
118    /// *do these two scopes touch* is an intersection. Fails if a name is not
119    /// one of these moves.
120    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
150/// Reads a whole reasoning out of a store. Nothing is run and no repository is
151/// touched: it is what was written down, plus what follows from it.
152pub 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    // A name that resolves to nothing is a move somebody wrote an edge to and
161    // then could not read back — dropped rather than drawn as a blank, since a
162    // box with no name is worse than an edge that is not there.
163    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
218/// The lines somebody abandoned, one row per root a decision named.
219///
220/// Only the decision that **won** the root gets a row: pursuing a line again is
221/// deciding again, and yesterday's abandonment is still written with its reason
222/// without being what folds today.
223fn 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            // What actually folds, and not everything below: a branch that was
242            // taken up again is under an abandoned root and is not hidden.
243            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
270/// An indented outline of what is there, the way the terminal prints it.
271///
272/// One line per move, because what an outline is read for is the shape. Here
273/// and not in the command for the reason the whole module is: an outline that
274/// disagreed with the figure about what folds would be two tools.
275pub 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        // What hangs nowhere is a root, which is how a move nobody hung is
288        // drawn: work waiting for a place, not a move that hides.
289        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
302/// How much of a move's prose fits on its line before it is cut.
303const 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    // A move under two parents is written under both, and its subtree once: the
315    // second reading says where it also belongs without repeating the branch.
316    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
349/// The first line of some prose, cut where a terminal stops reading it.
350fn 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}