Skip to main content

somatize_tree/
moves.rs

1//! The reasoning: questions, hypotheses, attempts, findings and decisions.
2//!
3//! Layer 1 — commits, snapshots, findings per node, trials — answers *what was
4//! run and what came out*. This answers *what somebody was trying to find out*,
5//! and shares none of its units: a commit is nobody's decision, a question
6//! nobody tried has no commit, and one move can produce three branches. What
7//! decides which layer something belongs to: if it can be recomputed it is
8//! record, and if somebody thought it, it is reasoning.
9//!
10//! It is a **DAG**, and one case forces it. Two live questions — does more
11//! capacity improve interpretability? does it improve performance? — one
12//! variant validating each, and then the question neither contained: what if I
13//! put them together? That attempt hangs under **both**. One parent would mean
14//! choosing, or duplicating the node, and a duplicated node is two that go out
15//! of step. Hence [`Undernath`] being multivalued, and hence refusing cycles as
16//! they are written: a walk over one does not end.
17//!
18//! Everything carries a scope, including what is said. A question is about
19//! some moves and not the whole investigation, and an answer holds **where it
20//! holds**. Without that, *validated* and *refuted* on one hypothesis look like
21//! a contradiction when normally they are two facts about two situations — A
22//! alone worked, A+B cancel out. There is a dispute only when two edges of
23//! opposite sign have scopes that **touch**.
24
25use serde::{Deserialize, Serialize};
26use somatize_store::{Bound as Record, Digest, Meta, Store};
27use std::collections::{BTreeMap, BTreeSet, HashSet};
28use std::fmt;
29
30/// How many slots to try before giving up. More than one turn only when
31/// somebody claimed the same one in the same instant: a race, not a queue.
32const PATIENCE: u32 = 32;
33
34/// What identifies a move. Its slot, because a move is mutable — you reword
35/// its prose — and so cannot be addressed by its content.
36pub type MoveId = u32;
37
38/// The five kinds, and there are no more.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum Kind {
42    /// What is not known. It gets **answered**. The only kind that can exist
43    /// with nothing under it: a question nobody tried is work outstanding.
44    Question,
45    /// A proposed, falsifiable answer. It gets **validated** or **refuted** —
46    /// verbs a question does not have, which is why it is not a question
47    /// reworded.
48    Hypothesis,
49    /// What was tried, citing layer 1. The only kind that touches it.
50    Attempt,
51    /// What the evidence says. The verb edges come from here, and it is the
52    /// only kind exportable to a knowledge lake.
53    Finding,
54    /// What is done about it. Apart from the finding because two people can
55    /// agree on one and disagree on the other.
56    Decision,
57}
58
59impl Kind {
60    /// `a` or `an`, for reading this kind into a sentence.
61    ///
62    /// Of the five only `Attempt` takes `an`, and it is the one these messages
63    /// reach for most: the commonest of them is `go` refusing a move that cites
64    /// no commit, which only an attempt ever could. So `a attempt` was the
65    /// article almost everybody saw.
66    pub fn article(&self) -> &'static str {
67        match self {
68            Self::Attempt => "an",
69            _ => "a",
70        }
71    }
72
73    pub fn read(said: &str) -> Option<Self> {
74        match said {
75            "question" => Some(Self::Question),
76            "hypothesis" => Some(Self::Hypothesis),
77            "attempt" => Some(Self::Attempt),
78            "finding" => Some(Self::Finding),
79            "decision" => Some(Self::Decision),
80            _ => None,
81        }
82    }
83
84    pub fn as_str(&self) -> &'static str {
85        match self {
86            Self::Question => "question",
87            Self::Hypothesis => "hypothesis",
88            Self::Attempt => "attempt",
89            Self::Finding => "finding",
90            Self::Decision => "decision",
91        }
92    }
93}
94
95impl fmt::Display for Kind {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(self.as_str())
98    }
99}
100
101/// What something is about: some moves and whatever hangs under them.
102///
103/// **Roots and not a free set**, which is what makes it affordable. *The whole
104/// encoder branch* is a root, *this step* is a root, the whole investigation is
105/// none. An arbitrary set would be truer and would turn *do they overlap?* into
106/// something to materialise rather than walk.
107#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(transparent)]
109pub struct Scope(pub BTreeSet<MoveId>);
110
111impl Scope {
112    /// About everything. What makes a general question general.
113    pub fn everything() -> Self {
114        Self(BTreeSet::new())
115    }
116
117    pub fn of(roots: impl IntoIterator<Item = MoveId>) -> Self {
118        Self(roots.into_iter().collect())
119    }
120
121    pub fn is_everything(&self) -> bool {
122        self.0.is_empty()
123    }
124
125    /// The moves it covers: its roots and everything hanging under them.
126    pub fn covers(&self, under: &Undernath) -> HashSet<MoveId> {
127        let mut reached = HashSet::new();
128        let mut asking: Vec<MoveId> = self.0.iter().copied().collect();
129        while let Some(one) = asking.pop() {
130            if !reached.insert(one) {
131                continue;
132            }
133            asking.extend(under.children_of(one));
134        }
135        reached
136    }
137
138    /// Whether two scopes touch. What separates a contradiction from two facts
139    /// about two different situations.
140    pub fn touches(&self, other: &Self, under: &Undernath) -> bool {
141        // What covers everything touches everything, including another such.
142        if self.is_everything() || other.is_everything() {
143            return true;
144        }
145        let mine = self.covers(under);
146        other.covers(under).iter().any(|one| mine.contains(one))
147    }
148}
149
150/// What a finding says, and towards what.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "kebab-case")]
153pub enum Says {
154    /// Towards a question.
155    Answers,
156    /// Towards a hypothesis.
157    Validates,
158    /// Towards a hypothesis.
159    Refutes,
160    /// From an attempt towards the attempts it composes. Not `under`: it says
161    /// this attempt **is** the composition of those, which is what lets *each
162    /// worked alone, together they cancel* read as what it is.
163    Combines,
164}
165
166impl Says {
167    pub fn read(said: &str) -> Option<Self> {
168        match said {
169            "answers" => Some(Self::Answers),
170            "validates" => Some(Self::Validates),
171            "refutes" => Some(Self::Refutes),
172            "combines" => Some(Self::Combines),
173            _ => None,
174        }
175    }
176
177    pub fn as_str(&self) -> &'static str {
178        match self {
179            Self::Answers => "answers",
180            Self::Validates => "validates",
181            Self::Refutes => "refutes",
182            Self::Combines => "combines",
183        }
184    }
185
186    /// Who may say it and to whom: a `validates` pointing at an attempt means
187    /// nothing, and accepting it stores a sentence nobody can read.
188    fn between(&self) -> (&'static [Kind], &'static [Kind]) {
189        match self {
190            Self::Answers => (&[Kind::Finding], &[Kind::Question]),
191            Self::Validates | Self::Refutes => (&[Kind::Finding], &[Kind::Hypothesis]),
192            Self::Combines => (&[Kind::Attempt], &[Kind::Attempt]),
193        }
194    }
195}
196
197impl fmt::Display for Says {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.write_str(self.as_str())
200    }
201}
202
203/// One thing said from a move towards another.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct Said {
206    pub from: MoveId,
207    pub to: MoveId,
208    pub says: Says,
209    /// Where it holds. Almost never everywhere, and that is the point.
210    #[serde(default)]
211    pub scope: Scope,
212    /// Whether it settles the question or only pushes it. *Does more capacity
213    /// help?* is not settled at once: three attempts each answer part.
214    #[serde(default)]
215    pub in_part: bool,
216}
217
218/// What was decided about the line a decision is about.
219///
220/// This used to be a verdict stuck on a commit — `promising`, `dead-end`,
221/// `superseded` — and it was never a property of the code. Here it has what it
222/// lacked there: a **scope** saying which line it is about, a **reason** in the
223/// prose, and a place in the DAG under the question it was answering. `invalid`
224/// is not here and will not be: that one really is the code's, and it stays in
225/// the journal.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "kebab-case")]
228pub enum Course {
229    /// Carry on this way. The default reading of a line nobody judged, so
230    /// saying it is only needed to take an abandonment back.
231    Pursue,
232    /// Explored and not worth carrying on. Kept, never deleted: a line that
233    /// did not work is the most reusable thing an investigation produces, and
234    /// the only thing that stops it being discovered again.
235    Abandon,
236    /// Somebody did it better elsewhere. Not wrong, not the way.
237    Superseded,
238}
239
240impl Course {
241    pub fn read(said: &str) -> Option<Self> {
242        match said {
243            "pursue" => Some(Self::Pursue),
244            "abandon" => Some(Self::Abandon),
245            "superseded" => Some(Self::Superseded),
246            _ => None,
247        }
248    }
249
250    pub fn as_str(&self) -> &'static str {
251        match self {
252            Self::Pursue => "pursue",
253            Self::Abandon => "abandon",
254            Self::Superseded => "superseded",
255        }
256    }
257}
258
259impl fmt::Display for Course {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.write_str(self.as_str())
262    }
263}
264
265/// A move, without its edges.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct Move {
268    pub id: MoveId,
269    /// What its author calls it, unique within the tree.
270    ///
271    /// The id is the store's — it says what order these were written in, and
272    /// it works as an identity for exactly as long as somebody is holding it
273    /// in a variable. Picking an investigation up again is the ordinary case:
274    /// another process, a tool call, or the same person a week later, none of
275    /// whom remember that the capacity question was `7`. So a move is reached
276    /// by a word somebody chose, and that is what makes `go` possible at all.
277    pub name: String,
278    pub kind: Kind,
279    /// What it is about. Only questions and hypotheses carry one; in the rest
280    /// it is everything and is not read.
281    #[serde(default)]
282    pub scope: Scope,
283    pub prose: String,
284    /// What it cites of layer 1: commits, trials, artifacts.
285    ///
286    /// Carried by an attempt — the commit that ran, and the trials it ran — and
287    /// by a finding — the trial it was seen in. A question, a hypothesis and a
288    /// decision are about moves and not about layer-1 pieces, and letting them
289    /// cite would let a question point at a commit with nobody knowing what
290    /// that means.
291    #[serde(default)]
292    pub cites: Vec<Cited>,
293    /// What was decided. Only a [`Kind::Decision`] carries one; in the rest it
294    /// is `None` and is not read.
295    #[serde(default)]
296    pub course: Option<Course>,
297    pub who: String,
298    pub when: u64,
299}
300
301/// A move somebody is writing, before the store gives it an id.
302///
303/// A struct and not seven arguments: four callers are coming — asking,
304/// trying, finding and deciding — and each cares about a different subset, so
305/// positionally they would be four call sites of `None, Vec::new(), None`
306/// where nobody can see which blank is which.
307pub struct Writing<'a> {
308    pub kind: Kind,
309    /// Unique within the tree. See [`Move::name`].
310    pub name: &'a str,
311    pub prose: &'a str,
312    pub who: &'a str,
313    /// Everything, unless this is a question, a hypothesis or a decision.
314    pub scope: Scope,
315    /// Only an attempt and a finding may carry one.
316    pub cites: Vec<Cited>,
317    /// Only a decision may carry one.
318    pub course: Option<Course>,
319}
320
321impl<'a> Writing<'a> {
322    /// The ordinary case: about everything, citing nothing, deciding nothing.
323    pub fn new(kind: Kind, name: &'a str, prose: &'a str, who: &'a str) -> Self {
324        Self {
325            kind,
326            name,
327            prose,
328            who,
329            scope: Scope::everything(),
330            cites: Vec::new(),
331            course: None,
332        }
333    }
334}
335
336/// One piece of evidence from layer 1.
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338pub struct Cited {
339    /// `commit`, `trial`, `artifact`. Open on purpose: the vocabulary is the
340    /// citer's, and this layer keeps it without learning it.
341    pub what: String,
342    pub id: String,
343}
344
345/// How a question stands, counting what has been said to it.
346///
347/// **Derived, never stored.** A *state* field somebody overwrites loses the
348/// previous fact, and the previous fact is what makes a hypothesis go back to
349/// open on its own when what refuted it is invalidated.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "kebab-case")]
352pub enum Standing {
353    /// Nobody has said anything yet.
354    Open,
355    /// Answered, and fully.
356    Answered,
357    /// Pushed along: everything that reached it said *in part*.
358    Partly,
359    Validated,
360    PartlyValidated,
361    Refuted,
362    PartlyRefuted,
363    /// Edges of opposite sign reach it **with scopes that touch**. The
364    /// interesting state, and the one a field cannot express.
365    Disputed,
366    /// Validated in some situations and refuted in others, without touching.
367    ///
368    /// Not *in part* and not a dispute: the answer **depends**. *A alone
369    /// improves, A+B cancel out* is the most informative outcome an
370    /// investigation gives, and calling it `Partly` hid it under the word for a
371    /// half-answered question.
372    Depends,
373}
374
375impl Standing {
376    pub fn as_str(&self) -> &'static str {
377        match self {
378            Self::Open => "open",
379            Self::Answered => "answered",
380            Self::Partly => "partly",
381            Self::Validated => "validated",
382            Self::PartlyValidated => "partly-validated",
383            Self::Refuted => "refuted",
384            Self::PartlyRefuted => "partly-refuted",
385            Self::Disputed => "disputed",
386            Self::Depends => "depends",
387        }
388    }
389}
390
391impl fmt::Display for Standing {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        f.write_str(self.as_str())
394    }
395}
396
397/// Who hangs under whom. An index over the `under` edges, built so it can be
398/// walked up and down without scanning again.
399#[derive(Debug, Default)]
400pub struct Undernath {
401    over: BTreeMap<MoveId, BTreeSet<MoveId>>,
402    below: BTreeMap<MoveId, BTreeSet<MoveId>>,
403}
404
405impl Undernath {
406    pub fn add(&mut self, child: MoveId, parent: MoveId) {
407        self.over.entry(child).or_default().insert(parent);
408        self.below.entry(parent).or_default().insert(child);
409    }
410
411    pub fn parents_of(&self, child: MoveId) -> Vec<MoveId> {
412        self.over
413            .get(&child)
414            .into_iter()
415            .flatten()
416            .copied()
417            .collect()
418    }
419
420    pub fn children_of(&self, parent: MoveId) -> Vec<MoveId> {
421        self.below
422            .get(&parent)
423            .into_iter()
424            .flatten()
425            .copied()
426            .collect()
427    }
428
429    /// Whether `maybe` is above `one`, looking upwards.
430    ///
431    /// What it takes to refuse a cycle before writing it: with `under`
432    /// multivalued the shape can no longer be trusted, and a cycle hangs every
433    /// later walk — including the one that would draw it.
434    pub fn is_over(&self, maybe: MoveId, one: MoveId) -> bool {
435        let mut seen = HashSet::new();
436        let mut asking = vec![one];
437        while let Some(which) = asking.pop() {
438            if which == maybe && !seen.is_empty() {
439                return true;
440            }
441            if !seen.insert(which) {
442                continue;
443            }
444            asking.extend(self.parents_of(which));
445        }
446        false
447    }
448}
449
450/// The reasoning of one investigation, kept in a store.
451pub struct Moves<'a> {
452    kept: &'a dyn Store,
453    tree: String,
454}
455
456impl<'a> Moves<'a> {
457    pub fn of(tree: impl Into<String>, kept: &'a dyn Store) -> Self {
458        Self {
459            kept,
460            tree: tree.into(),
461        }
462    }
463
464    fn named(&self, id: MoveId, what: &str, nth: u32) -> String {
465        format!("exp/{}/move/{id}/{what}/{nth}", self.tree)
466    }
467
468    /// Writes a new move and returns its id.
469    ///
470    /// Claims the slot exactly as a trial does: no coordinator, and whoever
471    /// finds it taken asks for the next. Two people writing at once get two
472    /// moves, not one lost.
473    pub fn add(&self, writing: Writing<'_>) -> Result<MoveId, Trouble> {
474        let Writing {
475            kind,
476            name,
477            prose,
478            who,
479            scope,
480            cites,
481            course,
482        } = writing;
483        if course.is_some() && kind != Kind::Decision {
484            return Err(Trouble::NotADecision { kind });
485        }
486        let name = name.trim();
487        let first = self.all()?.keys().copied().max().map_or(0, |last| last + 1);
488
489        // The name is claimed before anything is written, and `claim` and not
490        // read-then-write: between reading that a name is free and taking it,
491        // somebody else does the same, and two moves answer to one word while
492        // the store says nothing. It is the same primitive that hands out a
493        // trial, for the same reason.
494        //
495        // It is claimed at the id we mean to take, and rebound below if the
496        // slot loop had to move on. If that loop gives up altogether the name
497        // is left held, pointing at a move that was never written — which
498        // `went` reports as a name reaching nothing rather than as silence.
499        let held = self.holds(name, first)?;
500        if let Some(by) = held {
501            return Err(Trouble::NameTaken {
502                name: name.to_string(),
503                by,
504            });
505        }
506
507        for id in first..first + PATIENCE {
508            let body = Move {
509                id,
510                name: name.to_string(),
511                kind,
512                scope: scope.clone(),
513                prose: prose.trim().to_string(),
514                cites: cites.clone(),
515                course,
516                who: who.to_string(),
517                when: 0,
518            };
519            let bytes =
520                serde_json::to_vec(&body).map_err(|why| Trouble::Garbled(why.to_string()))?;
521            let digest = self.kept.put(&bytes).map_err(Trouble::Store)?;
522            let mut meta: Meta = vec![
523                ("what".into(), "move".into()),
524                ("kind".into(), kind.to_string()),
525                ("who".into(), who.to_string()),
526            ];
527            if let Some(course) = course {
528                meta.push(("course".into(), course.to_string()));
529            }
530            if self
531                .kept
532                .claim(&self.named(id, "said", 0), &digest, meta)
533                .map_err(Trouble::Store)?
534            {
535                if id != first {
536                    // Ours to overwrite: nobody else got past the claim above.
537                    self.point(name, id)?;
538                }
539                return Ok(id);
540            }
541        }
542        Err(Trouble::Crowded)
543    }
544
545    /// Where a name lives, which is its own record and not a scan of the moves.
546    ///
547    /// A name is asked far more often than the whole reasoning is drawn — every
548    /// `go`, every `--under` — and answering it by walking every move would
549    /// make the cheapest question in the tool cost the most expensive read.
550    fn calls(&self, name: &str) -> String {
551        format!("exp/{}/named/{name}", self.tree)
552    }
553
554    /// Takes the name for this id, or says who already has it.
555    fn holds(&self, name: &str, id: MoveId) -> Result<Option<MoveId>, Trouble> {
556        let digest = self
557            .kept
558            .put(id.to_string().as_bytes())
559            .map_err(Trouble::Store)?;
560        let meta: Meta = vec![
561            ("what".into(), "names".into()),
562            ("move".into(), id.to_string()),
563        ];
564        match self
565            .kept
566            .claim(&self.calls(name), &digest, meta)
567            .map_err(Trouble::Store)?
568        {
569            true => Ok(None),
570            false => Ok(Some(self.went(name)?)),
571        }
572    }
573
574    /// Points an already-held name at the id it ended up with.
575    fn point(&self, name: &str, id: MoveId) -> Result<(), Trouble> {
576        let digest = self
577            .kept
578            .put(id.to_string().as_bytes())
579            .map_err(Trouble::Store)?;
580        let meta: Meta = vec![
581            ("what".into(), "names".into()),
582            ("move".into(), id.to_string()),
583        ];
584        self.kept
585            .bind(&self.calls(name), &digest, meta)
586            .map_err(Trouble::Store)
587    }
588
589    /// The move that name reaches. One lookup, no scan.
590    pub fn went(&self, name: &str) -> Result<MoveId, Trouble> {
591        let name = name.trim();
592        let bound = self
593            .kept
594            .resolve(&self.calls(name))
595            .map_err(Trouble::Store)?
596            .ok_or_else(|| Trouble::NoSuchName {
597                name: name.to_string(),
598            })?;
599        let bytes = self
600            .kept
601            .get(&bound.digest)
602            .map_err(Trouble::Store)?
603            .ok_or_else(|| Trouble::NoSuchName {
604                name: name.to_string(),
605            })?;
606        String::from_utf8_lossy(&bytes)
607            .trim()
608            .parse()
609            .map_err(|_| Trouble::Garbled(format!("`{name}` does not name a move")))
610    }
611
612    /// Rewords a move. A new slot, and the last wins: what came before is
613    /// still there, as in the journal.
614    ///
615    /// What arrives as `None` stays as it was, so correcting the prose does not
616    /// wipe the scope or the other way round. The scope **has** to be
617    /// correctable: in a decision it says which line is meant, and getting it
618    /// wrong — reaching a finding, which is not a line, instead of the attempt
619    /// it came from — leaves the decision reaching no commit at all with nothing
620    /// warning. A course changes but is never removed: a decision that decides
621    /// nothing any more is `pursue`.
622    pub fn reword(
623        &self,
624        id: MoveId,
625        prose: Option<&str>,
626        scope: Option<Scope>,
627        course: Option<Course>,
628        who: &str,
629    ) -> Result<u32, Trouble> {
630        let mut body = self.all()?.remove(&id).ok_or(Trouble::NoSuchMove { id })?;
631        if course.is_some() && body.kind != Kind::Decision {
632            return Err(Trouble::NotADecision { kind: body.kind });
633        }
634        if let Some(prose) = prose {
635            body.prose = prose.trim().to_string();
636        }
637        if let Some(scope) = scope {
638            body.scope = scope;
639        }
640        if course.is_some() {
641            body.course = course;
642        }
643        self.redrafted(id, body, who)
644    }
645
646    /// Writes one drafting of a move into the next slot.
647    fn redrafted(&self, id: MoveId, mut body: Move, who: &str) -> Result<u32, Trouble> {
648        body.who = who.to_string();
649        let bytes = serde_json::to_vec(&body).map_err(|why| Trouble::Garbled(why.to_string()))?;
650        let digest = self.kept.put(&bytes).map_err(Trouble::Store)?;
651        let first = self.slots(id, "said")? + 1;
652        for nth in first..first + PATIENCE {
653            let mut meta: Meta = vec![
654                ("what".into(), "move".into()),
655                ("kind".into(), body.kind.to_string()),
656                ("who".into(), who.to_string()),
657            ];
658            // The same meta `add` writes and not a poorer one: a record that
659            // says less than the one before lies about what is underneath.
660            if let Some(course) = body.course {
661                meta.push(("course".into(), course.to_string()));
662            }
663            if self
664                .kept
665                .claim(&self.named(id, "said", nth), &digest, meta)
666                .map_err(Trouble::Store)?
667            {
668                return Ok(nth);
669            }
670        }
671        Err(Trouble::Crowded)
672    }
673
674    /// Adds one piece of evidence to a move.
675    ///
676    /// A new drafting and the last wins, as with everything here: evidence
677    /// arrives after the attempt is written, because the trials run afterwards.
678    /// Citing the same thing twice does not duplicate it — two people looking
679    /// at one screen would ask for it, and a list with a trial twice says
680    /// nothing a list with it once does not.
681    pub fn cite(&self, id: MoveId, cited: Cited, who: &str) -> Result<u32, Trouble> {
682        let known = self.all()?;
683        let body = known.get(&id).ok_or(Trouble::NoSuchMove { id })?;
684        if !matches!(body.kind, Kind::Attempt | Kind::Finding) {
685            return Err(Trouble::CannotCite { kind: body.kind });
686        }
687        if body.cites.contains(&cited) {
688            return self.slots(id, "said");
689        }
690        let mut body = body.clone();
691        body.cites.push(cited);
692        self.redrafted(id, body, who)
693    }
694
695    /// Hangs a move under another.
696    ///
697    /// The cycle is refused here, the only place it is cheap: reading it later
698    /// means discovering it by having a walk hang.
699    pub fn hang(&self, child: MoveId, parent: MoveId) -> Result<(), Trouble> {
700        if child == parent {
701            return Err(Trouble::Circular { child, parent });
702        }
703        let known = self.all()?;
704        for one in [child, parent] {
705            if !known.contains_key(&one) {
706                return Err(Trouble::NoSuchMove { id: one });
707            }
708        }
709        if self.under()?.is_over(child, parent) {
710            return Err(Trouble::Circular { child, parent });
711        }
712        self.bind(
713            child,
714            "under",
715            &parent.to_string(),
716            &[("parent", &parent.to_string())],
717        )
718    }
719
720    /// Says something from a move towards another.
721    pub fn say(&self, said: Said) -> Result<(), Trouble> {
722        let known = self.all()?;
723        let (from, to) = (
724            known
725                .get(&said.from)
726                .ok_or(Trouble::NoSuchMove { id: said.from })?,
727            known
728                .get(&said.to)
729                .ok_or(Trouble::NoSuchMove { id: said.to })?,
730        );
731        let (says_from, says_to) = said.says.between();
732        if !says_from.contains(&from.kind) || !says_to.contains(&to.kind) {
733            return Err(Trouble::Nonsense {
734                says: said.says,
735                from: from.kind,
736                to: to.kind,
737            });
738        }
739        let body = serde_json::to_vec(&said).map_err(|why| Trouble::Garbled(why.to_string()))?;
740        let digest = self.kept.put(&body).map_err(Trouble::Store)?;
741        let target = said.to.to_string();
742        let says = said.says.to_string();
743        self.bound(
744            said.from,
745            "says",
746            &digest,
747            &[("says", says.as_str()), ("to", target.as_str())],
748        )
749    }
750
751    /// Every move, by id, with its latest drafting.
752    pub fn all(&self) -> Result<BTreeMap<MoveId, Move>, Trouble> {
753        let under = format!("exp/{}/move/", self.tree);
754        let mut latest: BTreeMap<MoveId, (u32, Digest, u64)> = BTreeMap::new();
755        for bound in self.kept.bound().map_err(Trouble::Store)? {
756            // A store holds whatever anybody put in it — a cache, another
757            // investigation, an artifact — so this is a question.
758            let Some(rest) = bound.name.strip_prefix(&under) else {
759                continue;
760            };
761            let Some((id, nth)) = rest.split_once("/said/") else {
762                continue;
763            };
764            let (Ok(id), Ok(nth)) = (id.parse::<MoveId>(), nth.parse::<u32>()) else {
765                continue;
766            };
767            match latest.get(&id) {
768                Some((had, _, _)) if *had >= nth => {}
769                _ => {
770                    latest.insert(id, (nth, bound.digest, bound.when));
771                }
772            }
773        }
774
775        let mut said = BTreeMap::new();
776        for (id, (_, digest, when)) in latest {
777            let Some(bytes) = self.kept.get(&digest).map_err(Trouble::Store)? else {
778                continue;
779            };
780            if let Ok(mut body) = serde_json::from_slice::<Move>(&bytes) {
781                body.when = when;
782                said.insert(id, body);
783            }
784        }
785        Ok(said)
786    }
787
788    /// The index of who hangs under whom.
789    pub fn under(&self) -> Result<Undernath, Trouble> {
790        let mut said = Undernath::default();
791        for (child, bound) in self.records("under")? {
792            // From the record and not the name: a name's last segment is the
793            // **slot**, and reading it as the parent builds an index that looks
794            // right and points at moves that do not exist.
795            if let Some(parent) = beside(&bound.meta, "parent").and_then(|one| one.parse().ok()) {
796                said.add(child, parent);
797            }
798        }
799        Ok(said)
800    }
801
802    /// Everything anybody said from one move towards another.
803    ///
804    /// The last wins per `(from, to, verb)`, the same rule a drafting follows
805    /// in `all`. Without it a scope could not be corrected: saying it again
806    /// would leave both edges and the count would take both, so widening a
807    /// scope would look like saying it twice. Saying it again **is** the gesture
808    /// for changing your mind about a scope; withdrawing the verb entirely
809    /// still has no gesture.
810    ///
811    /// The triple comes from the meta and not the body, so keeping the last one
812    /// needs no read of the earlier ones.
813    pub fn says(&self) -> Result<Vec<Said>, Trouble> {
814        let under = format!("exp/{}/move/", self.tree);
815        let mut latest: BTreeMap<(MoveId, String, String), (u32, Digest)> = BTreeMap::new();
816        for bound in self.kept.bound().map_err(Trouble::Store)? {
817            let Some(rest) = bound.name.strip_prefix(&under) else {
818                continue;
819            };
820            let Some((from, nth)) = rest.split_once("/says/") else {
821                continue;
822            };
823            let (Ok(from), Ok(nth)) = (from.parse::<MoveId>(), nth.parse::<u32>()) else {
824                continue;
825            };
826            let (Some(verb), Some(to)) = (
827                beside(&bound.meta, "says").map(str::to_string),
828                beside(&bound.meta, "to").map(str::to_string),
829            ) else {
830                continue;
831            };
832            match latest.get(&(from, verb.clone(), to.clone())) {
833                Some((had, _)) if *had >= nth => {}
834                _ => {
835                    latest.insert((from, verb, to), (nth, bound.digest));
836                }
837            }
838        }
839
840        let mut said = Vec::new();
841        for (_, digest) in latest.into_values() {
842            let Some(bytes) = self.kept.get(&digest).map_err(Trouble::Store)? else {
843                continue;
844            };
845            if let Ok(one) = serde_json::from_slice::<Said>(&bytes) {
846                said.push(one);
847            }
848        }
849        Ok(said)
850    }
851
852    /// What was decided about each commit, derived from the reasoning.
853    ///
854    /// The bridge between the layers, and it runs this way: a commit does not
855    /// store that it is abandoned. It is reached by going down — decision, its
856    /// scope, the attempts that scope covers, the commits those attempts cite —
857    /// so a commit made tomorrow under an abandoned line comes out abandoned
858    /// with nobody writing anything again.
859    ///
860    /// **A decision with no scope is about where it hangs**, which is where it
861    /// parts company with a question or a hypothesis, for which no scope means
862    /// about everything. In a decision that would be a quiet trap: writing
863    /// *this line is dead* while looking at one attempt would mark the whole
864    /// tree. Abandoning everything means hanging it off the root or naming it.
865    ///
866    /// The last wins: changing your mind is deciding again, and yesterday's
867    /// abandonment is still written with its reason.
868    pub fn decided(&self) -> Result<BTreeMap<String, Course>, Trouble> {
869        let known = self.all()?;
870        let under = self.under()?;
871        // Whoever spoke last about a commit is the one that counts, and the
872        // speaker is a decision: carried through rather than left to the order
873        // the moves happen to be visited in.
874        let mut said: BTreeMap<String, (MoveId, Course)> = BTreeMap::new();
875        for (one, (by, course)) in coursed(&known, &under) {
876            let Some(reached) = known.get(&one) else {
877                continue;
878            };
879            for cited in &reached.cites {
880                if cited.what != "commit" {
881                    continue;
882                }
883                match said.get(&cited.id) {
884                    Some((said_by, _)) if *said_by > by => {}
885                    _ => {
886                        said.insert(cited.id.clone(), (by, course));
887                    }
888                }
889            }
890        }
891        Ok(said
892            .into_iter()
893            .map(|(commit, (_, course))| (commit, course))
894            .collect())
895    }
896
897    /// What was decided about each **move**, and by which decision.
898    ///
899    /// The half of [`decided`](Self::decided) that never reaches a commit, and
900    /// the one the reasoning is drawn from: an attempt nobody ran cites nothing
901    /// and is still abandoned.
902    pub fn courses(&self) -> Result<BTreeMap<MoveId, (MoveId, Course)>, Trouble> {
903        Ok(coursed(&self.all()?, &self.under()?))
904    }
905
906    /// How each question and hypothesis stands, counting what reached it.
907    ///
908    /// What was [`withdrawn`](Self::withdrawn) does not count, which is what
909    /// makes a hypothesis go back to `open` on its own.
910    pub fn standing(&self) -> Result<BTreeMap<MoveId, Standing>, Trouble> {
911        let known = self.all()?;
912        let under = self.under()?;
913        let says = self.says()?;
914        let withdrawn = self.withdrawn()?;
915        Ok(known
916            .iter()
917            .filter(|(_, body)| matches!(body.kind, Kind::Question | Kind::Hypothesis))
918            .map(|(id, body)| {
919                let mine: Vec<&Said> = says
920                    .iter()
921                    .filter(|one| one.to == *id && !withdrawn.contains(&one.from))
922                    .collect();
923                (*id, stands(body.kind, &mine, &under))
924            })
925            .collect())
926    }
927
928    /// The moves whose evidence was judged wrong, so what they said no longer
929    /// counts towards a standing.
930    ///
931    /// **This is where the two layers meet, and it only runs this way.** A
932    /// verdict is about the code — `invalid` is deliberately not a `Course` —
933    /// and a finding read off a measurement that lied has nothing behind it.
934    /// Nothing is deleted: the edge is still written and still drawn, and a
935    /// later `sound` puts it back, because the journal keeps the last word and
936    /// a standing is worked out rather than overwritten.
937    ///
938    /// The **direct** verdict only. A commit under an invalid one inherits
939    /// doubt and not a judgement — `walk` already draws that line — and
940    /// inheriting it needs an ancestry nothing here asks git for.
941    ///
942    /// It reaches **up the DAG** and not only at the move itself: a finding
943    /// usually cites the trial it was seen in and hangs under the attempt, and
944    /// it is the attempt that names the commit. Walking up can only ever pick
945    /// up an attempt or a finding, since those are the only kinds that cite.
946    pub fn withdrawn(&self) -> Result<BTreeSet<MoveId>, Trouble> {
947        let judged = crate::journal::Journal::of(self.tree.clone(), self.kept)
948            .verdicts()
949            .map_err(|why| Trouble::Garbled(why.to_string()))?;
950        let known = self.all()?;
951        let under = self.under()?;
952        let void = |id: &MoveId| {
953            known.get(id).is_some_and(|body| {
954                body.cites.iter().any(|cited| {
955                    cited.what == "commit"
956                        && judged.get(&cited.id) == Some(&crate::journal::Verdict::Invalid)
957                })
958            })
959        };
960        Ok(known
961            .keys()
962            .filter(|id| {
963                let mut seen = HashSet::new();
964                let mut asking = vec![**id];
965                while let Some(one) = asking.pop() {
966                    if !seen.insert(one) {
967                        continue;
968                    }
969                    if void(&one) {
970                        return true;
971                    }
972                    asking.extend(under.parents_of(one));
973                }
974                false
975            })
976            .copied()
977            .collect())
978    }
979
980    /// The prose behind a citation, or whatever was kept there.
981    pub fn read(&self, digest: &Digest) -> Result<Option<Vec<u8>>, Trouble> {
982        self.kept.get(digest).map_err(Trouble::Store)
983    }
984
985    /// The records under `exp/<tree>/move/<id>/<what>/…`, with their id.
986    ///
987    /// The whole record and not just the name: what is needed of an edge — who
988    /// it points at — is in its meta, which a scan brings back free.
989    fn records(&self, what: &str) -> Result<Vec<(MoveId, Record)>, Trouble> {
990        let under = format!("exp/{}/move/", self.tree);
991        let mark = format!("/{what}/");
992        Ok(self
993            .kept
994            .bound()
995            .map_err(Trouble::Store)?
996            .into_iter()
997            .filter_map(|bound| {
998                let rest = bound.name.strip_prefix(&under)?;
999                let (id, _) = rest.split_once(&mark)?;
1000                Some((id.parse().ok()?, bound))
1001            })
1002            .collect())
1003    }
1004
1005    /// How many slots of one kind are taken under a move.
1006    fn slots(&self, id: MoveId, what: &str) -> Result<u32, Trouble> {
1007        let mark = format!("/move/{id}/{what}/");
1008        Ok(self
1009            .records(what)?
1010            .iter()
1011            .filter(|(which, bound)| *which == id && bound.name.contains(&mark))
1012            .filter_map(|(_, bound)| bound.name.rsplit('/').next()?.parse::<u32>().ok())
1013            .max()
1014            .unwrap_or(0))
1015    }
1016
1017    /// Claims a slot for a fact with no body of its own: the name is the data.
1018    fn bind(
1019        &self,
1020        id: MoveId,
1021        what: &str,
1022        body: &str,
1023        meta: &[(&str, &str)],
1024    ) -> Result<(), Trouble> {
1025        let digest = self.kept.put(body.as_bytes()).map_err(Trouble::Store)?;
1026        self.bound(id, what, &digest, meta)
1027    }
1028
1029    fn bound(
1030        &self,
1031        id: MoveId,
1032        what: &str,
1033        digest: &Digest,
1034        meta: &[(&str, &str)],
1035    ) -> Result<(), Trouble> {
1036        let first = self.slots(id, what)? + 1;
1037        for nth in first..first + PATIENCE {
1038            let meta: Meta = meta
1039                .iter()
1040                .map(|(a, b)| (a.to_string(), b.to_string()))
1041                .collect();
1042            if self
1043                .kept
1044                .claim(&self.named(id, what, nth), digest, meta)
1045                .map_err(Trouble::Store)?
1046            {
1047                return Ok(());
1048            }
1049        }
1050        Err(Trouble::Crowded)
1051    }
1052}
1053
1054/// What each move's line was decided to be, and by which decision.
1055///
1056/// Oldest first, which here is the order of the ids: deciding again is how you
1057/// change your mind, and the later decision is the one that counts.
1058fn coursed(
1059    known: &BTreeMap<MoveId, Move>,
1060    under: &Undernath,
1061) -> BTreeMap<MoveId, (MoveId, Course)> {
1062    let mut said = BTreeMap::new();
1063    for (id, body) in known {
1064        let Some(course) = body.course else { continue };
1065        let scope = abandoning(*id, body, under);
1066        if scope.is_everything() {
1067            continue;
1068        }
1069        for one in scope.covers(under) {
1070            said.insert(one, (*id, course));
1071        }
1072    }
1073    said
1074}
1075
1076/// What a decision is about: its scope, or where it hangs when it has none.
1077///
1078/// Where a decision parts company with a question or a hypothesis, for which no
1079/// scope means about everything. Here that would be a quiet trap: writing *this
1080/// line is dead* while looking at one attempt would mark the whole tree.
1081/// Abandoning everything means hanging it off the root or naming it — and a
1082/// decision hanging off nothing with no scope colours no line rather than all.
1083pub(crate) fn abandoning(id: MoveId, body: &Move, under: &Undernath) -> Scope {
1084    match body.scope.is_everything() {
1085        true => Scope::of(under.parents_of(id)),
1086        false => body.scope.clone(),
1087    }
1088}
1089
1090/// How a question stands given what has been said to it.
1091///
1092/// The scopes do the work: two edges of opposite sign are a contradiction only
1093/// if they are about situations that touch. *A alone worked* and *A+B cancel
1094/// out* are two facts, not a conflict.
1095fn stands(kind: Kind, said: &[&Said], under: &Undernath) -> Standing {
1096    if said.is_empty() {
1097        return Standing::Open;
1098    }
1099    if kind == Kind::Question {
1100        let answers: Vec<&&Said> = said
1101            .iter()
1102            .filter(|one| one.says == Says::Answers)
1103            .collect();
1104        return match answers.iter().any(|one| !one.in_part) {
1105            true => Standing::Answered,
1106            false if answers.is_empty() => Standing::Open,
1107            false => Standing::Partly,
1108        };
1109    }
1110
1111    let (yes, no): (Vec<&&Said>, Vec<&&Said>) = said
1112        .iter()
1113        .filter(|one| matches!(one.says, Says::Validates | Says::Refutes))
1114        .partition(|one| one.says == Says::Validates);
1115    if yes.is_empty() && no.is_empty() {
1116        return Standing::Open;
1117    }
1118    // Dispute is measured by overlap and not by presence: if nobody is talking
1119    // about the same thing, there is nothing to dispute.
1120    let disputed = yes
1121        .iter()
1122        .any(|a| no.iter().any(|b| a.scope.touches(&b.scope, under)));
1123    if disputed {
1124        return Standing::Disputed;
1125    }
1126    match (yes.is_empty(), no.is_empty()) {
1127        (false, true) if yes.iter().any(|one| !one.in_part) => Standing::Validated,
1128        (false, true) => Standing::PartlyValidated,
1129        (true, false) if no.iter().any(|one| !one.in_part) => Standing::Refuted,
1130        (true, false) => Standing::PartlyRefuted,
1131        // Both signs without touching: the answer depends on where you look.
1132        _ => Standing::Depends,
1133    }
1134}
1135
1136/// One field of a record, if it is there.
1137fn beside<'a>(meta: &'a Meta, what: &str) -> Option<&'a str> {
1138    meta.iter()
1139        .find(|(said, _)| said == what)
1140        .map(|(_, value)| value.as_str())
1141}
1142
1143#[derive(Debug)]
1144pub enum Trouble {
1145    Store(somatize_store::StoreError),
1146    Garbled(String),
1147    NoSuchMove { id: MoveId },
1148    Circular { child: MoveId, parent: MoveId },
1149    Nonsense { says: Says, from: Kind, to: Kind },
1150    NotADecision { kind: Kind },
1151    NameTaken { name: String, by: MoveId },
1152    NoSuchName { name: String },
1153    CannotCite { kind: Kind },
1154    Crowded,
1155}
1156
1157impl fmt::Display for Trouble {
1158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1159        match self {
1160            Self::Store(why) => write!(f, "the reasoning could not be reached: {why}"),
1161            Self::Garbled(why) => write!(f, "something could not be written or read: {why}"),
1162            Self::NoSuchMove { id } => write!(f, "there is no move {id}"),
1163            Self::Circular { child, parent } => write!(
1164                f,
1165                "hanging {child} under {parent} would make a cycle, and a walk over one does not end"
1166            ),
1167            Self::Nonsense { says, from, to } => {
1168                // No article in front of `{says}`: it is a quoted verb, and
1169                // `answers` would want `an` while `refutes` wants `a`.
1170                let (a, b) = (from.article(), to.article());
1171                write!(f, "`{says}` from {a} {from} to {b} {to} means nothing")
1172            }
1173            Self::NotADecision { kind } => {
1174                write!(
1175                    f,
1176                    "a course is carried by a decision, and this is {} {kind}",
1177                    kind.article()
1178                )
1179            }
1180            Self::NameTaken { name, by } => write!(
1181                f,
1182                "`{name}` already names move {by}. A name is how a move is found again,                  so two of them answering to one word would be a move nobody can reach"
1183            ),
1184            Self::NoSuchName { name } => write!(f, "nothing here is called `{name}`"),
1185            Self::CannotCite { kind } => write!(
1186                f,
1187                "{} {kind} is about moves and not about commits or trials: citing belongs \
1188                 to an attempt or a finding",
1189                kind.article()
1190            ),
1191            Self::Crowded => write!(f, "too many people writing at once"),
1192        }
1193    }
1194}
1195
1196impl std::error::Error for Trouble {}