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 {}