1use std::collections::{HashMap, HashSet};
57use std::hash::{BuildHasherDefault, Hasher};
58use std::io::{Read, Seek, SeekFrom};
59use std::os::unix::fs::MetadataExt;
60use std::path::{Path, PathBuf};
61
62use crate::memory::{self, Folded};
63use crate::outcome::{self, Outcome, Shared};
64
65pub const FILE: &str = "beliefs.bin";
67pub const REGRETS_FILE: &str = "beliefs-regrets.bin";
69const MAGIC: &[u8; 8] = b"BORGFCT2";
70const REGRETS_MAGIC: &[u8; 8] = b"BORGRGT2";
71const WINDOW: u64 = 4096;
73
74type Id = u32;
76type Fp = u128;
78
79fn fp(s: &str) -> Fp {
80 u128::from_le_bytes(blake3::hash(s.as_bytes()).as_bytes()[..16].try_into().expect("16 bytes"))
81}
82
83fn rules() -> [u8; 32] {
85 *blake3::hash(concat!("borg-facts\0", env!("CARGO_PKG_VERSION")).as_bytes()).as_bytes()
86}
87
88#[derive(Default, Clone, Copy)]
93pub struct Fx(u64);
94
95impl Fx {
96 fn add(&mut self, word: u64) {
97 self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95);
98 }
99}
100
101impl Hasher for Fx {
102 fn write(&mut self, bytes: &[u8]) {
103 let (words, rest) = bytes.as_chunks::<8>();
104 for w in words {
105 self.add(u64::from_le_bytes(*w));
106 }
107 if !rest.is_empty() {
108 let mut w = [0u8; 8];
109 w[..rest.len()].copy_from_slice(rest);
110 self.add(u64::from_le_bytes(w) ^ (rest.len() as u64) << 56);
111 }
112 }
113 fn write_u8(&mut self, x: u8) {
114 self.add(u64::from(x));
115 }
116 fn write_u32(&mut self, x: u32) {
117 self.add(u64::from(x));
118 }
119 fn write_u64(&mut self, x: u64) {
120 self.add(x);
121 }
122 fn write_u128(&mut self, x: u128) {
123 self.add(x as u64);
124 self.add((x >> 64) as u64);
125 }
126 fn write_usize(&mut self, x: usize) {
127 self.add(x as u64);
128 }
129 fn finish(&self) -> u64 {
130 self.0
131 }
132}
133
134pub type FxMap<K, V> = HashMap<K, V, BuildHasherDefault<Fx>>;
136pub type FxSet<K> = HashSet<K, BuildHasherDefault<Fx>>;
137
138#[derive(Debug, Default, Clone, PartialEq)]
140struct Sess {
141 tick: Option<i64>,
143 repos: Vec<Id>,
145 corrections: Vec<i64>,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
151struct Cell {
152 r: i8,
154 used: bool,
156 tested: bool,
158 trust: u8,
160 trust_ms: i64,
161}
162
163impl Default for Cell {
164 fn default() -> Self {
165 Cell { r: -1, used: false, tested: false, trust: 0, trust_ms: 0 }
166 }
167}
168
169#[derive(Debug, Default, Clone, PartialEq)]
171struct Check {
172 dev: u64,
173 ino: u64,
174 at: u64,
175 head: [u8; 32],
176 tail: [u8; 32],
177}
178
179#[derive(Debug, Default, Clone, PartialEq)]
181struct Part {
182 check: Check,
183 sessions: FxMap<Id, Sess>,
184 cells: FxMap<(Id, Id), Cell>,
185 use_repos: FxSet<(Id, Id, Id)>,
187 tested: FxSet<(Id, Vec<Id>)>,
190 given: FxSet<(Id, Id)>,
191}
192
193impl Part {
194 fn cell(&mut self, l: Id, s: Id) -> &mut Cell {
195 self.cells.entry((l, s)).or_default()
196 }
197}
198
199type Merged = (f64, u64, Id);
201type Placed = (f64, (bool, u64), Id);
203
204#[derive(Debug, Default, Clone, PartialEq)]
206struct Regrets {
207 off: FxMap<(Id, Fp), Merged>,
210 on: FxMap<(Id, Fp), Merged>,
211 team: FxMap<(Id, Fp, Id), (f64, u64)>,
214}
215
216impl Regrets {
217 fn is_empty(&self) -> bool {
218 self.off.is_empty() && self.on.is_empty() && self.team.is_empty()
219 }
220}
221
222fn merge(m: &mut FxMap<(Id, Fp), Merged>, key: (Id, Fp), credit: f64, off: u64, s: Id) {
224 let e = m.entry(key).or_insert((0.0, off, s));
225 e.0 = e.0.max(credit);
226 if off < e.1 {
227 (e.1, e.2) = (off, s);
228 }
229}
230
231#[derive(Debug, Default, Clone)]
233pub struct Facts {
234 strings: Vec<String>,
235 index: HashMap<String, Id>,
237 tags: FxMap<Id, Id>,
239 local: Part,
240 team: Part,
241 mine: FxSet<Id>,
243 blame: [Vec<(Id, Id, f64)>; 2],
245 generation: u128,
247 regrets: Option<Regrets>,
249 regrets_file: Option<PathBuf>,
250 changed: bool,
252 stale: bool,
254 lost: bool,
256}
257
258impl PartialEq for Facts {
260 fn eq(&self, o: &Self) -> bool {
261 (&self.strings, &self.tags, &self.local, &self.team, &self.blame) == (&o.strings, &o.tags, &o.local, &o.team, &o.blame)
262 }
263}
264
265struct Rec<'a> {
267 kind: &'a str,
268 session: &'a str,
269 lessons: &'a [String],
270 ms: i64,
271 repo: &'a str,
272 channel: &'a str,
273 cause: &'a str,
274 credit: f64,
275}
276
277#[cfg(test)]
278thread_local! {
279 static PARSED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
281 static REGRET_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
282}
283
284impl Facts {
285 fn id(&mut self, s: &str) -> Id {
286 if self.index.len() != self.strings.len() {
287 self.index = self.strings.iter().enumerate().map(|(i, s)| (s.clone(), i as Id)).collect();
288 }
289 if let Some(i) = self.index.get(s) {
290 return *i;
291 }
292 let i = self.strings.len() as Id;
293 self.strings.push(s.to_string());
294 self.index.insert(s.to_string(), i);
295 i
296 }
297
298 fn tagged(&mut self, raw: Id) -> Id {
300 if let Some(t) = self.tags.get(&raw) {
301 return *t;
302 }
303 let s = &self.strings[raw as usize];
304 let t = if s.is_empty() { String::new() } else { outcome::tag(s) };
305 let t = self.id(&t);
306 self.tags.insert(raw, t);
307 t
308 }
309
310 fn regrets(&mut self) -> Option<&mut Regrets> {
313 if self.regrets.is_none() {
314 let read = match self.generation {
315 0 => Some(Regrets::default()),
316 generation => {
317 #[cfg(test)]
318 REGRET_READS.with(|c| c.set(c.get() + 1));
319 let bytes = self.regrets_file.as_deref().and_then(|p| std::fs::read(p).ok());
320 bytes.and_then(|b| decode_regrets(&b, generation, self.strings.len()))
321 }
322 };
323 match read {
324 Some(r) => self.regrets = Some(r),
325 None => {
326 self.lost = true;
327 return None;
328 }
329 }
330 }
331 self.regrets.as_mut()
332 }
333
334 fn forget(&mut self, team: bool) {
337 if team {
338 match self.regrets() {
339 Some(r) if !r.team.is_empty() => r.team.clear(),
340 _ => return,
341 }
342 } else {
343 self.regrets = Some(Regrets::default());
344 }
345 (self.changed, self.stale) = (true, true);
346 }
347
348 fn fold(&mut self, team: bool, bytes: &[u8], base: u64) {
350 let mut pos = 0;
351 for line in bytes.split(|b| *b == b'\n') {
352 let off = base + pos as u64;
353 pos += line.len() + 1;
354 #[cfg(test)]
355 PARSED.with(|c| c.set(c.get() + 1));
356 let line = line.strip_suffix(b"\r").unwrap_or(line);
358 let Ok(text) = std::str::from_utf8(line) else { continue };
359 if team {
360 let Ok(x) = serde_json::from_str::<Shared>(text) else { continue };
361 if outcome::SHARED_KINDS.contains(&x.kind.as_str()) {
362 let rec =
363 Rec { kind: &x.kind, session: &x.s, lessons: &x.lessons, ms: x.ms, repo: &x.r, channel: &x.channel, cause: &x.c, credit: x.credit };
364 self.add(true, &rec, off);
365 }
366 } else if let Ok(o) = serde_json::from_str::<Outcome>(text) {
367 let rec = Rec {
368 kind: &o.kind,
369 session: &o.session,
370 lessons: &o.lessons,
371 ms: o.ms,
372 repo: &o.repo,
373 channel: &o.channel,
374 cause: &o.cause,
375 credit: o.credit,
376 };
377 self.add(false, &rec, off);
378 }
379 }
380 }
381
382 fn add(&mut self, team: bool, o: &Rec<'_>, off: u64) {
384 let s = self.id(o.session);
385 if !team {
386 let t = self.tagged(s);
387 if self.mine.insert(t) && self.team.sessions.contains_key(&t) && self.regrets().is_some() {
389 self.stale = true;
390 }
391 self.local.sessions.entry(s).or_default();
392 } else if self.mine.contains(&s) {
393 return;
397 }
398 let lessons: Vec<Id> = o.lessons.iter().map(|l| self.id(l)).collect();
399 let repo = self.id(o.repo);
400 if !team {
401 self.tagged(repo);
402 }
403 if team && o.kind != outcome::TESTED {
404 self.team.given.extend(lessons.iter().map(|l| (s, *l)));
405 }
406 if o.channel == outcome::PLACEBO || matches!(o.kind, outcome::UNCLEAR | outcome::REMAINS | outcome::NOT_ARISEN) {
408 return;
409 }
410 let part = if team { &mut self.team } else { &mut self.local };
411 let sess = part.sessions.entry(s).or_default();
412 if memory::ticks(o.kind) {
413 sess.tick = Some(sess.tick.map_or(o.ms, |t| t.min(o.ms)));
414 if !o.repo.is_empty() && !sess.repos.contains(&repo) {
415 sess.repos.push(repo);
416 }
417 }
418 if memory::is_correction_of(o.kind, o.cause) && !sess.corrections.contains(&o.ms) {
419 sess.corrections.push(o.ms);
420 }
421 if o.kind == outcome::TESTED {
422 if team {
423 part.tested.insert((s, lessons));
424 } else {
425 lessons.iter().for_each(|l| part.cell(*l, s).tested = true);
426 }
427 return;
428 }
429 if o.kind == outcome::REGRETTED {
430 let credit = o.credit.clamp(0.0, 1.0);
431 let named = if o.cause.is_empty() { String::new() } else { outcome::tag(o.cause) };
434 let (raw, named) = (fp(o.cause), fp(&named));
435 let Some(r) = self.regrets() else { return };
436 for l in lessons {
437 if team {
438 let e = r.team.entry((l, raw, s)).or_insert((0.0, off));
439 e.0 = e.0.max(credit);
440 e.1 = e.1.min(off);
441 } else {
442 merge(&mut r.off, (l, raw), credit, off, s);
443 merge(&mut r.on, (l, named), credit, off, s);
444 }
445 }
446 (self.changed, self.stale) = (true, true);
447 return;
448 }
449 let trust = match o.kind {
450 outcome::CORRECTED | outcome::VETOED => 2,
451 outcome::NOT_CORRECTED => 1,
452 _ => 0,
453 };
454 let r = memory::result_of(o.kind);
455 if r.is_none() && trust == 0 {
456 return;
457 }
458 for l in lessons {
459 let c = part.cell(l, s);
460 if let Some(r) = r {
461 c.r = c.r.max(r as i8);
462 c.used |= r == 1;
463 }
464 if trust > 0 {
465 c.trust_ms = if c.trust == 0 { o.ms } else { c.trust_ms.min(o.ms) };
466 c.trust = c.trust.max(trust);
467 }
468 if r == Some(1) {
469 part.use_repos.insert((l, s, repo));
470 }
471 }
472 }
473
474 fn settle(&mut self) {
477 if !self.stale {
478 return;
479 }
480 let Some(r) = self.regrets.as_ref() else { return };
481 let off = memory::blame(r.off.iter().map(|((l, _), (credit, _, s))| (*l, *s, *credit)));
482 let mut on: FxMap<(Id, Fp), Placed> = FxMap::default();
483 on.reserve(r.on.len() + r.team.len());
484 on.extend(r.on.iter().map(|(k, (credit, off, s))| (*k, (*credit, (false, *off), *s))));
485 for ((l, c, s), (credit, off)) in r.team.iter().filter(|((_, _, s), _)| !self.mine.contains(s)) {
486 let order = (true, *off);
487 let e = on.entry((*l, *c)).or_insert((0.0, order, *s));
488 e.0 = e.0.max(*credit);
489 if order < e.1 {
490 (e.1, e.2) = (order, *s);
491 }
492 }
493 let on = memory::blame(on.into_iter().map(|((l, _), (credit, _, s))| (l, s, credit)));
494 self.blame = [off, on];
495 self.stale = false;
496 }
497}
498
499pub struct View<'a> {
501 pub folded: Folded<'a>,
502 pub trust: HashMap<&'a str, HashMap<&'a str, (bool, i64)>>,
504}
505
506impl Facts {
507 pub fn view(&self, on: bool) -> View<'_> {
509 let st = |i: Id| self.strings[i as usize].as_str();
510 let local = |i: Id| if on { self.tags.get(&i).copied().unwrap_or(i) } else { i };
512 let named = |team: bool, i: Id| if team { i } else { local(i) };
513 let parts: &[(&Part, bool)] = if on { &[(&self.local, false), (&self.team, true)] } else { &[(&self.local, false)] };
514 let keep = |team: bool, s: Id| !team || !self.mine.contains(&s);
515 let mut f = Folded::default();
516 let mut trust: HashMap<&str, HashMap<&str, (bool, i64)>> = HashMap::new();
517 let mut ticks: FxMap<Id, (i64, FxSet<Id>)> = FxMap::default();
518 for (p, team) in parts.iter().copied() {
519 for (s, x) in p.sessions.iter().filter(|(s, _)| keep(team, **s)) {
520 if !x.corrections.is_empty() {
521 f.corrections.entry(st(*s)).or_default().extend(x.corrections.iter().copied());
522 }
523 if let Some(tick) = x.tick {
524 let e = ticks.entry(*s).or_insert((tick, FxSet::default()));
525 e.0 = e.0.min(tick);
526 e.1.extend(x.repos.iter().map(|r| named(team, *r)));
527 }
528 }
529 for ((l, s), c) in p.cells.iter().filter(|((_, s), _)| keep(team, *s)) {
530 let (l, s) = (st(*l), st(*s));
531 if c.r >= 0 {
532 let e = f.result.entry(l).or_default().entry(s).or_insert(c.r as u8);
533 *e = (*e).max(c.r as u8);
534 }
535 if c.used {
536 f.uses.entry(l).or_default().insert(s);
537 }
538 if c.tested {
539 f.tested.entry(l).or_default().insert(s);
540 }
541 if c.trust > 0 {
542 let e = trust.entry(l).or_default().entry(s).or_insert((c.trust == 2, c.trust_ms));
543 e.0 |= c.trust == 2;
544 e.1 = e.1.min(c.trust_ms);
545 }
546 }
547 for (l, _, r) in p.use_repos.iter().filter(|(_, s, _)| keep(team, *s)) {
548 f.repos.entry(st(*l)).or_default().insert(st(named(team, *r)));
549 }
550 for (s, ls) in p.tested.iter().filter(|(s, _)| keep(team, *s)) {
551 if ls.iter().all(|l| p.given.contains(&(*s, *l))) {
552 for l in ls {
553 f.tested.entry(st(*l)).or_default().insert(st(*s));
554 }
555 }
556 }
557 }
558 let clock = ticks.into_iter().map(|(s, (ms, repos))| (st(s).to_string(), ms, repos.into_iter().map(|r| st(r).to_string()).collect()));
559 f.clock = memory::Clock::from_sessions(clock);
560 f.regret = self.blame[usize::from(on)].iter().map(|(l, s, credit)| (st(*l), st(*s), *credit)).collect();
561 View { folded: f, trust }
562 }
563}
564
565fn window(f: &mut std::fs::File, from: u64, to: u64) -> [u8; 32] {
567 let mut buf = vec![0u8; (to - from) as usize];
568 match f.seek(SeekFrom::Start(from)).and_then(|_| f.read_exact(&mut buf)) {
569 Ok(()) => *blake3::hash(&buf).as_bytes(),
570 Err(_) => [0xff; 32],
571 }
572}
573
574struct Caught {
576 moved: bool,
578 rebuilt: bool,
580 rest: Vec<u8>,
582 at: u64,
583}
584
585impl Facts {
586 fn part(&mut self, team: bool) -> &mut Part {
587 if team { &mut self.team } else { &mut self.local }
588 }
589
590 fn catch_up(&mut self, team: bool, log: &Path) -> Caught {
593 let Ok(mut f) = std::fs::File::open(log) else {
594 let gone = *self.part(team) != Part::default();
595 if gone {
596 *self.part(team) = Part::default();
597 self.forget(team);
598 }
599 return Caught { moved: gone, rebuilt: gone, rest: Vec::new(), at: 0 };
600 };
601 let Ok(meta) = f.metadata() else { return Caught { moved: false, rebuilt: false, rest: Vec::new(), at: 0 } };
602 let len = meta.len();
603 let c = self.part(team).check.clone();
604 let same = c.at == 0
605 || (c.dev == meta.dev()
606 && c.ino == meta.ino()
607 && len >= c.at
608 && window(&mut f, 0, c.at.min(WINDOW)) == c.head
609 && window(&mut f, c.at.saturating_sub(WINDOW), c.at) == c.tail);
610 let rebuilt = !same;
611 if rebuilt {
612 *self.part(team) = Part::default();
613 self.forget(team);
614 }
615 let at = self.part(team).check.at;
616 let mut rest = Vec::new();
617 if f.seek(SeekFrom::Start(at)).and_then(|_| f.read_to_end(&mut rest)).is_err() {
618 return Caught { moved: rebuilt, rebuilt, rest: Vec::new(), at };
619 }
620 let done = rest.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
621 let tail = rest.split_off(done);
622 if done > 0 {
623 self.fold(team, &rest[..done - 1], at);
624 let to = at + done as u64;
625 let (head, end) = (window(&mut f, 0, to.min(WINDOW)), window(&mut f, to.saturating_sub(WINDOW), to));
626 self.part(team).check = Check { dev: meta.dev(), ino: meta.ino(), at: to, head, tail: end };
627 }
628 Caught { moved: rebuilt || done > 0, rebuilt, rest: tail, at: at + done as u64 }
629 }
630
631 fn catch_up_store(&mut self, lessons: &Path) -> (bool, [(Vec<u8>, u64); 2]) {
634 let local = self.catch_up(false, &outcome::path_beside(lessons));
635 if local.rebuilt {
636 self.team = Part::default();
639 self.mine = self.local.sessions.keys().filter_map(|s| self.tags.get(s).copied()).collect();
640 }
641 let team = self.catch_up(true, &lessons.with_file_name(outcome::TEAM_FILE));
642 (local.moved || team.moved, [(local.rest, local.at), (team.rest, team.at)])
643 }
644
645 fn fold_rests(&mut self, rests: [(Vec<u8>, u64); 2]) {
647 for (team, (rest, at)) in [false, true].into_iter().zip(rests) {
648 if !rest.is_empty() {
649 self.fold(team, &rest, at);
650 }
651 }
652 }
653
654 fn update(mut self, lessons: &Path, save: bool) -> Option<Facts> {
657 self.regrets_file = Some(regrets_beside(lessons));
658 let (moved, rests) = self.catch_up_store(lessons);
659 if self.lost {
660 return None;
661 }
662 self.settle();
663 if moved && save {
664 store(lessons, &mut self);
665 }
666 self.fold_rests(rests);
667 self.settle();
668 (!self.lost).then_some(self)
669 }
670}
671
672pub fn path_beside(lessons: &Path) -> PathBuf {
674 lessons.with_file_name(FILE)
675}
676
677pub fn regrets_beside(lessons: &Path) -> PathBuf {
679 lessons.with_file_name(REGRETS_FILE)
680}
681
682pub fn load(lessons: &Path) -> Facts {
685 let saved = std::fs::read(path_beside(lessons)).ok().and_then(|b| decode(&b)).unwrap_or_default();
686 saved.update(lessons, true).or_else(|| Facts::default().update(lessons, true)).unwrap_or_default()
688}
689
690#[cfg(test)]
692fn fresh(lessons: &Path) -> Facts {
693 Facts::default().update(lessons, false).expect("a fold from nothing reads no regrets file")
694}
695
696pub fn rebuild(lessons: &Path) {
699 let mut facts = Facts { regrets_file: Some(regrets_beside(lessons)), ..Facts::default() };
700 let (moved, _) = facts.catch_up_store(lessons);
701 facts.settle();
702 if moved {
703 store(lessons, &mut facts);
704 }
705}
706
707fn write_atomic(path: &Path, bytes: &[u8]) -> bool {
709 let tmp = path.with_extension(format!("{}.tmp", std::process::id()));
710 let ok = std::fs::write(&tmp, bytes).is_ok() && std::fs::rename(&tmp, path).is_ok();
711 if !ok {
712 let _ = std::fs::remove_file(&tmp);
713 }
714 ok
715}
716
717fn new_generation() -> u128 {
719 static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
720 let mut h = blake3::Hasher::new();
721 h.update(&std::process::id().to_le_bytes());
722 h.update(&std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos()).to_le_bytes());
723 h.update(&N.fetch_add(1, std::sync::atomic::Ordering::Relaxed).to_le_bytes());
724 u128::from_le_bytes(h.finalize().as_bytes()[..16].try_into().expect("16 bytes")).max(1)
725}
726
727fn store(lessons: &Path, facts: &mut Facts) {
730 if facts.changed {
731 let Some(r) = facts.regrets.as_ref() else { return };
732 let generation = match r.is_empty() {
733 true => 0,
734 false => new_generation(),
735 };
736 if generation != 0 && !write_atomic(®rets_beside(lessons), &encode_regrets(r, generation)) {
737 return;
738 }
739 (facts.generation, facts.changed) = (generation, false);
740 }
741 write_atomic(&path_beside(lessons), &encode(facts));
742}
743
744struct W(Vec<u8>);
746
747impl W {
748 fn u8(&mut self, x: u8) {
749 self.0.push(x);
750 }
751 fn u32(&mut self, x: u32) {
752 self.0.extend_from_slice(&x.to_le_bytes());
753 }
754 fn u64(&mut self, x: u64) {
755 self.0.extend_from_slice(&x.to_le_bytes());
756 }
757 fn u128(&mut self, x: u128) {
758 self.0.extend_from_slice(&x.to_le_bytes());
759 }
760 fn len(&mut self, n: usize) {
761 self.u32(n as u32);
762 }
763 fn seal(mut self) -> Vec<u8> {
765 let sum = blake3::hash(&self.0);
766 self.0.extend_from_slice(sum.as_bytes());
767 self.0
768 }
769}
770
771struct R<'a>(&'a [u8]);
773
774impl<'a> R<'a> {
775 fn open(b: &'a [u8], magic: &[u8; 8]) -> Option<Self> {
777 let (body, sum) = b.split_at_checked(b.len().checked_sub(32)?)?;
778 if blake3::hash(body).as_bytes() != sum {
779 return None;
780 }
781 let mut r = R(body);
782 (r.take(magic.len())? == magic && r.hash()? == rules()).then_some(r)
783 }
784 fn take(&mut self, n: usize) -> Option<&'a [u8]> {
785 let (a, b) = self.0.split_at_checked(n)?;
786 self.0 = b;
787 Some(a)
788 }
789 fn u8(&mut self) -> Option<u8> {
790 Some(self.take(1)?[0])
791 }
792 fn u32(&mut self) -> Option<u32> {
793 Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
794 }
795 fn u64(&mut self) -> Option<u64> {
796 Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?))
797 }
798 fn u128(&mut self) -> Option<u128> {
799 Some(u128::from_le_bytes(self.take(16)?.try_into().ok()?))
800 }
801 fn i64(&mut self) -> Option<i64> {
802 self.u64().map(|x| x as i64)
803 }
804 fn f64(&mut self) -> Option<f64> {
805 self.u64().map(f64::from_bits)
806 }
807 fn hash(&mut self) -> Option<[u8; 32]> {
808 self.take(32)?.try_into().ok()
809 }
810 fn len(&mut self, each: usize) -> Option<usize> {
813 let n = self.u32()? as usize;
814 (n.checked_mul(each)? <= self.0.len()).then_some(n)
815 }
816 fn id(&mut self, n: usize) -> Option<Id> {
818 self.u32().filter(|i| (*i as usize) < n)
819 }
820}
821
822fn enc_part(w: &mut W, p: &Part) {
823 let c = &p.check;
824 w.u64(c.dev);
825 w.u64(c.ino);
826 w.u64(c.at);
827 w.0.extend_from_slice(&c.head);
828 w.0.extend_from_slice(&c.tail);
829 w.len(p.sessions.len());
830 for (s, x) in &p.sessions {
831 w.u32(*s);
832 w.u8(u8::from(x.tick.is_some()));
833 w.u64(x.tick.unwrap_or(0) as u64);
834 w.len(x.repos.len());
835 x.repos.iter().for_each(|r| w.u32(*r));
836 w.len(x.corrections.len());
837 x.corrections.iter().for_each(|m| w.u64(*m as u64));
838 }
839 w.len(p.cells.len());
840 for ((l, s), c) in &p.cells {
841 w.u32(*l);
842 w.u32(*s);
843 w.u8(c.r as u8);
844 w.u8(u8::from(c.used) | u8::from(c.tested) << 1);
845 w.u8(c.trust);
846 w.u64(c.trust_ms as u64);
847 }
848 w.len(p.use_repos.len());
849 for (l, s, r) in &p.use_repos {
850 [*l, *s, *r].into_iter().for_each(|x| w.u32(x));
851 }
852 w.len(p.tested.len());
853 for (s, ls) in &p.tested {
854 w.u32(*s);
855 w.len(ls.len());
856 ls.iter().for_each(|l| w.u32(*l));
857 }
858 w.len(p.given.len());
859 for (s, l) in &p.given {
860 w.u32(*s);
861 w.u32(*l);
862 }
863}
864
865fn dec_part(r: &mut R<'_>, n: usize) -> Option<Part> {
867 let mut p = Part { check: Check { dev: r.u64()?, ino: r.u64()?, at: r.u64()?, head: r.hash()?, tail: r.hash()? }, ..Part::default() };
868 let k = r.len(21)?;
869 p.sessions.reserve(k);
870 for _ in 0..k {
871 let s = r.id(n)?;
872 let tick = match (r.u8()?, r.i64()?) {
873 (0, _) => None,
874 (1, ms) => Some(ms),
875 _ => return None,
876 };
877 let repos = (0..r.len(4)?).map(|_| r.id(n)).collect::<Option<_>>()?;
878 let corrections = (0..r.len(8)?).map(|_| r.i64()).collect::<Option<_>>()?;
879 p.sessions.insert(s, Sess { tick, repos, corrections });
880 }
881 let k = r.len(19)?;
882 p.cells.reserve(k);
883 for _ in 0..k {
884 let key = (r.id(n)?, r.id(n)?);
885 let (rv, flags, trust, trust_ms) = (r.u8()? as i8, r.u8()?, r.u8()?, r.i64()?);
886 p.cells.insert(key, Cell { r: rv, used: flags & 1 != 0, tested: flags & 2 != 0, trust, trust_ms });
887 }
888 for _ in 0..r.len(12)? {
889 p.use_repos.insert((r.id(n)?, r.id(n)?, r.id(n)?));
890 }
891 for _ in 0..r.len(8)? {
892 let s = r.id(n)?;
893 let ls = (0..r.len(4)?).map(|_| r.id(n)).collect::<Option<_>>()?;
894 p.tested.insert((s, ls));
895 }
896 for _ in 0..r.len(8)? {
897 p.given.insert((r.id(n)?, r.id(n)?));
898 }
899 Some(p)
900}
901
902pub fn encode(f: &Facts) -> Vec<u8> {
904 let mut w = W(Vec::with_capacity(1 << 16));
905 w.0.extend_from_slice(MAGIC);
906 w.0.extend_from_slice(&rules());
907 w.u128(f.generation);
908 w.len(f.strings.len());
909 for s in &f.strings {
910 w.len(s.len());
911 w.0.extend_from_slice(s.as_bytes());
912 }
913 w.len(f.tags.len());
914 for (a, b) in &f.tags {
915 w.u32(*a);
916 w.u32(*b);
917 }
918 enc_part(&mut w, &f.local);
919 enc_part(&mut w, &f.team);
920 for blame in &f.blame {
921 w.len(blame.len());
922 for (l, s, credit) in blame {
923 w.u32(*l);
924 w.u32(*s);
925 w.u64(credit.to_bits());
926 }
927 }
928 w.seal()
929}
930
931pub fn decode(b: &[u8]) -> Option<Facts> {
934 let mut r = R::open(b, MAGIC)?;
935 let mut f = Facts { generation: r.u128()?, ..Facts::default() };
936 let n = r.len(4)?;
937 f.strings.reserve(n);
938 for _ in 0..n {
939 let len = r.len(1)?;
940 f.strings.push(std::str::from_utf8(r.take(len)?).ok()?.to_string());
941 }
942 let k = r.len(8)?;
943 f.tags.reserve(k);
944 for _ in 0..k {
945 f.tags.insert(r.id(n)?, r.id(n)?);
946 }
947 f.local = dec_part(&mut r, n)?;
948 f.team = dec_part(&mut r, n)?;
949 for blame in &mut f.blame {
950 *blame = (0..r.len(16)?).map(|_| Some((r.id(n)?, r.id(n)?, r.f64()?))).collect::<Option<_>>()?;
951 }
952 f.mine = f.local.sessions.keys().map(|s| f.tags.get(s).copied()).collect::<Option<_>>()?;
953 r.0.is_empty().then_some(f)
954}
955
956fn encode_regrets(x: &Regrets, generation: u128) -> Vec<u8> {
958 let mut w = W(Vec::with_capacity(40 * (x.off.len() + x.on.len() + x.team.len()) + 128));
959 w.0.extend_from_slice(REGRETS_MAGIC);
960 w.0.extend_from_slice(&rules());
961 w.u128(generation);
962 for m in [&x.off, &x.on] {
963 w.len(m.len());
964 for ((l, c), (credit, off, s)) in m {
965 w.u32(*l);
966 w.u128(*c);
967 w.u64(credit.to_bits());
968 w.u64(*off);
969 w.u32(*s);
970 }
971 }
972 w.len(x.team.len());
973 for ((l, c, s), (credit, off)) in &x.team {
974 w.u32(*l);
975 w.u128(*c);
976 w.u32(*s);
977 w.u64(credit.to_bits());
978 w.u64(*off);
979 }
980 w.seal()
981}
982
983fn decode_regrets(b: &[u8], generation: u128, n: usize) -> Option<Regrets> {
986 let mut r = R::open(b, REGRETS_MAGIC)?;
987 (r.u128()? == generation).then_some(())?;
988 let mut x = Regrets::default();
989 for m in [&mut x.off, &mut x.on] {
990 let k = r.len(40)?;
991 m.reserve(k);
992 for _ in 0..k {
993 let key = (r.id(n)?, r.u128()?);
994 m.insert(key, (r.f64()?, r.u64()?, r.id(n)?));
995 }
996 }
997 let k = r.len(40)?;
998 x.team.reserve(k);
999 for _ in 0..k {
1000 let key = (r.id(n)?, r.u128()?, r.id(n)?);
1001 x.team.insert(key, (r.f64()?, r.u64()?));
1002 }
1003 r.0.is_empty().then_some(x)
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use crate::conscience::Challenge;
1010 use crate::memory::Rng;
1011
1012 fn dir(tag: &str) -> PathBuf {
1013 let d = std::env::temp_dir().join(format!("borg-facts-{tag}-{}", std::process::id()));
1014 let _ = std::fs::remove_dir_all(&d);
1015 std::fs::create_dir_all(&d).unwrap();
1016 d
1017 }
1018
1019 fn pick<'a>(r: &mut Rng, xs: &[&'a str]) -> &'a str {
1020 xs[(r.next_u64() % xs.len() as u64) as usize]
1021 }
1022
1023 fn logs(seed: u64) -> (String, String, Vec<Challenge>) {
1027 let mut r = Rng::new(seed);
1028 let kinds = [
1029 outcome::ASKED,
1030 outcome::CHANGED,
1031 outcome::IGNORED,
1032 outcome::CORRECTED,
1033 outcome::NOT_CORRECTED,
1034 outcome::MISSED,
1035 outcome::VETOED,
1036 outcome::REGRETTED,
1037 outcome::TESTED,
1038 outcome::UNCLEAR,
1039 outcome::REMAINS,
1040 outcome::NOT_ARISEN,
1041 outcome::GRADED,
1042 outcome::ENDED,
1043 ];
1044 let lessons = ["L0", "L1", "L2", "L3", "L4", "L5"];
1045 let repos = ["repoA", "RepoA", "repoB", ""];
1046 let causes = ["", "", "R1", "R2", "V3"];
1047 let mut ms = 0;
1048 let mut record = |r: &mut Rng, session: String| {
1049 ms += 1 + (r.next_u64() % 3) as i64;
1050 let kind = pick(r, &kinds);
1051 outcome::Outcome {
1052 kind: kind.into(),
1053 session,
1054 lessons: (0..r.next_u64() % 3 + u64::from(kind != outcome::ENDED)).map(|_| pick(r, &lessons).to_string()).collect(),
1055 ms: match r.next_u64() % 24 {
1058 0 => i64::MAX,
1059 1 => i64::MIN,
1060 2..=7 => 7,
1061 _ => ms,
1062 },
1063 repo: pick(r, &repos).into(),
1064 channel: if r.next_u64().is_multiple_of(6) { outcome::PLACEBO.into() } else { pick(r, &["", "early", "stop"]).into() },
1065 cause: if kind == outcome::REGRETTED || kind == outcome::MISSED { pick(r, &causes).into() } else { String::new() },
1066 credit: if kind == outcome::REGRETTED { (r.next_u64() % 16) as f64 / 10.0 - 0.2 } else { 0.0 },
1067 ..Default::default()
1068 }
1069 };
1070 let (mut local, mut team): (Vec<String>, Vec<String>) = (Vec::new(), Vec::new());
1071 for _ in 0..r.next_u64() % 40 {
1072 let line = match r.next_u64() % 12 {
1073 0 => "{\"kind\":\"asked\",\"session\":".to_string(),
1074 1 if !local.is_empty() => local[(r.next_u64() % local.len() as u64) as usize].clone(),
1075 _ => {
1076 let session = format!("s{}", r.next_u64() % 5);
1077 let o = record(&mut r, session);
1078 if let Some(x) = outcome::share(&o, "this-node").filter(|_| r.next_u64().is_multiple_of(2)) {
1079 team.push(serde_json::to_string(&x).unwrap());
1080 }
1081 serde_json::to_string(&o).unwrap()
1082 }
1083 };
1084 local.push(line);
1085 }
1086 for _ in 0..r.next_u64() % 40 {
1087 let line = match r.next_u64() % 12 {
1088 0 => "not a record".to_string(),
1089 1 if !team.is_empty() => team[(r.next_u64() % team.len() as u64) as usize].clone(),
1090 _ => {
1091 let mut o = record(&mut r, String::new());
1092 o.channel = String::new();
1093 let peer = format!("{:032x}", r.next_u64() % 4);
1094 let mut x = outcome::share(&o, "peer").unwrap_or_else(|| outcome::Shared { kind: o.kind.clone(), ms: o.ms, ..Default::default() });
1095 (x.s, x.lessons) = (peer, o.lessons.clone());
1096 serde_json::to_string(&x).unwrap()
1097 }
1098 };
1099 team.push(line);
1100 }
1101 let challenge = |k: usize| Challenge {
1102 id: if k < 6 { lessons[k].to_string() } else { format!("N{k}") },
1103 origin_ms: (r.next_u64() % 60) as i64,
1104 reach: if k.is_multiple_of(3) { crate::reach::REPO.into() } else { String::new() },
1105 repo: if k.is_multiple_of(3) { pick(&mut r, &repos).into() } else { String::new() },
1106 engineers: (0..1 + k % 2).map(|e| format!("e{e}")).collect(),
1107 ..Default::default()
1108 };
1109 let cs = (0..8).map(challenge).collect();
1110 let lines = |v: Vec<String>| v.into_iter().map(|l| l + "\n").collect::<String>();
1111 (lines(local), lines(team), cs)
1112 }
1113
1114 fn norm<A, B>(m: &HashMap<&str, HashMap<&str, (bool, i64), B>, A>) -> Vec<(String, String, bool, i64)> {
1116 let mut v: Vec<(String, String, bool, i64)> = m.iter().flat_map(|(l, ss)| ss.iter().map(|(s, x)| (l.to_string(), s.to_string(), x.0, x.1))).collect();
1117 v.sort();
1118 v
1119 }
1120
1121 fn same(lessons: &Path, cs: &[Challenge], facts: &Facts, what: &str) {
1124 for on in [true, false] {
1125 let outcomes = outcome::team_view(lessons, on);
1126 let view = facts.view(on);
1127 assert_eq!(norm(&outcome::sessions(&outcomes)), norm(&view.trust), "{what}: trust, on={on}");
1128 let mut sessions: Vec<Option<&str>> = outcomes.iter().map(|o| Some(o.session.as_str())).collect();
1129 sessions.sort();
1130 sessions.dedup();
1131 sessions.extend([None, Some("a-new-session")]);
1132 for session in sessions {
1133 let want = memory::derive(&outcomes, cs, session);
1134 let got = memory::derive_folded(facts.view(on).folded, cs, session);
1135 assert_eq!(want.len(), got.len());
1136 for (id, a) in &want {
1137 let b = &got[id];
1138 let close = |x: f64, y: f64| x == y || (x - y).abs() <= 1e-9 * x.abs().max(1.0);
1139 assert!(
1140 (a.changed, a.ignored, a.corrected, a.vetoed, a.tested, a.consolidated, a.dormant)
1141 == (b.changed, b.ignored, b.corrected, b.vetoed, b.tested, b.consolidated, b.dormant)
1142 && close(a.activation, b.activation)
1143 && close(a.regretted, b.regretted),
1144 "{what}: on={on} session={session:?} {id}: {a:?} != {b:?}"
1145 );
1146 }
1147 }
1148 }
1149 }
1150
1151 #[test]
1154 fn the_facts_equal_a_fresh_derive_however_the_logs_grew() {
1155 let d = dir("grow");
1156 let lessons = d.join("lessons.jsonl");
1157 let (pl, pt) = (outcome::path_beside(&lessons), d.join(outcome::TEAM_FILE));
1158 for seed in 0..200 {
1159 let (local, team, cs) = logs(seed);
1160 let _ = std::fs::remove_file(path_beside(&lessons));
1161 std::fs::write(&pl, "").unwrap();
1162 std::fs::write(&pt, "").unwrap();
1163 let mut r = Rng::new(seed ^ 0xfac7);
1164 let (mut l, mut t) = (0, 0);
1165 while l < local.len() || t < team.len() {
1166 l = (l + (r.next_u64() % 1200) as usize).min(local.len());
1167 t = (t + (r.next_u64() % 1200) as usize).min(team.len());
1168 use std::io::Write;
1169 let have = |p: &Path| std::fs::metadata(p).unwrap().len() as usize;
1170 std::fs::OpenOptions::new().append(true).open(&pl).unwrap().write_all(&local.as_bytes()[have(&pl)..l]).unwrap();
1171 std::fs::OpenOptions::new().append(true).open(&pt).unwrap().write_all(&team.as_bytes()[have(&pt)..t]).unwrap();
1172 let facts = load(&lessons);
1173 same(&lessons, &cs, &facts, &format!("seed {seed} at {l}/{t}"));
1174 }
1175 let saved = decode(&std::fs::read(path_beside(&lessons)).unwrap_or_default());
1176 if let Some(mut saved) = saved {
1177 assert_eq!(decode(&encode(&saved)).as_ref(), Some(&saved), "seed {seed}: round trip");
1178 saved.regrets_file = Some(regrets_beside(&lessons));
1179 let n = saved.strings.len();
1180 let regrets = saved.regrets().cloned().expect("the regrets of the snapshot's generation");
1181 assert_eq!(decode_regrets(&encode_regrets(®rets, 9), 9, n).as_ref(), Some(®rets), "seed {seed}: regrets round trip");
1182 }
1183 same(&lessons, &cs, &fresh(&lessons), &format!("seed {seed}: fresh"));
1184 same(&lessons, &cs, &load(&lessons), &format!("seed {seed}: warm"));
1185 }
1186 let _ = std::fs::remove_dir_all(d);
1187 }
1188
1189 #[test]
1193 fn a_rewritten_log_or_a_bad_snapshot_folds_afresh() {
1194 let d = dir("rewrite");
1195 let lessons = d.join("lessons.jsonl");
1196 let (pl, pt) = (outcome::path_beside(&lessons), d.join(outcome::TEAM_FILE));
1197 let snap = path_beside(&lessons);
1198 for seed in 0..60 {
1199 let (local, team, cs) = logs(seed);
1200 let _ = std::fs::remove_file(&snap);
1201 std::fs::write(&pl, &local).unwrap();
1202 std::fs::write(&pt, &team).unwrap();
1203 same(&lessons, &cs, &load(&lessons), "cold");
1204 let every_other = |text: &str, k: usize| text.lines().enumerate().filter(|(i, _)| i % 3 != k).map(|(_, l)| format!("{l}\n")).collect::<String>();
1205 let tmp = d.join("q.tmp");
1207 std::fs::write(&tmp, every_other(&team, 1)).unwrap();
1208 std::fs::rename(&tmp, &pt).unwrap();
1209 same(&lessons, &cs, &load(&lessons), "renamed");
1210 std::fs::write(&pl, every_other(&local, 0)).unwrap();
1212 same(&lessons, &cs, &load(&lessons), "local shorter");
1213 std::fs::write(&pt, every_other(&team, 2) + &team).unwrap();
1214 same(&lessons, &cs, &load(&lessons), "team longer");
1215 let lines: Vec<&str> = local.lines().collect();
1216 if lines.len() > 2 {
1217 let mut body: String = lines.iter().enumerate().filter(|(i, _)| *i != 1).map(|(_, l)| format!("{l}\n")).collect();
1218 body.push_str(&" ".repeat(lines[1].len()));
1219 body.push('\n');
1220 std::fs::write(&pl, &body).unwrap();
1221 same(&lessons, &cs, &load(&lessons), "local same length");
1222 }
1223 if let Ok(mut b) = std::fs::read(&snap) {
1225 let n = b.len() / 2;
1226 b[n] ^= 0x55;
1227 assert!(decode(&b).is_none());
1228 std::fs::write(&snap, &b).unwrap();
1229 same(&lessons, &cs, &load(&lessons), "corrupt");
1230 let b = std::fs::read(&snap).unwrap();
1231 std::fs::write(&snap, &b[..b.len() - 1]).unwrap();
1232 same(&lessons, &cs, &load(&lessons), "truncated");
1233 }
1234 std::fs::write(&snap, b"BORGFCT0 an older fold").unwrap();
1235 same(&lessons, &cs, &load(&lessons), "foreign");
1236 std::fs::remove_file(&pt).unwrap();
1238 same(&lessons, &cs, &load(&lessons), "team removed");
1239 std::fs::remove_file(&pl).unwrap();
1240 same(&lessons, &cs, &load(&lessons), "local removed");
1241 std::fs::write(&pl, &local).unwrap();
1242 std::fs::write(&pt, &team).unwrap();
1243 same(&lessons, &cs, &load(&lessons), "back");
1244 }
1245 let (local, team): (String, String) = (0..40).map(logs).map(|(l, t, _)| (l, t)).unzip();
1248 let cs = logs(0).2;
1249 let mid = local[..local.len() / 2].rfind('\n').unwrap() + 1;
1250 let sentinel =
1251 serde_json::to_string(&Outcome { kind: outcome::CHANGED.into(), session: "sx".into(), lessons: vec!["LX".into()], ms: 5, ..Default::default() })
1252 .unwrap();
1253 let local = format!("{}{sentinel}\n{}", &local[..mid], &local[mid..]);
1254 assert!(mid > 2 * WINDOW as usize && local.len() - mid > 2 * WINDOW as usize);
1255 let _ = std::fs::remove_file(&snap);
1256 std::fs::write(&pl, &local).unwrap();
1257 std::fs::write(&pt, &team).unwrap();
1258 let big = load(&lessons);
1259 same(&lessons, &cs, &big, "big");
1260 let uses_lx = |f: &Facts| f.view(false).folded.uses.contains_key("LX");
1261 assert!(uses_lx(&big));
1262 let mut edited = local.clone();
1263 edited.replace_range(mid..mid + sentinel.len(), &" ".repeat(sentinel.len()));
1264 std::fs::write(&pl, &edited).unwrap();
1265 assert!(load(&lessons) == big, "the warm load folds only what follows its checkpoint");
1266 assert!(!uses_lx(&fresh(&lessons)), "the edit changed a fact");
1267 rebuild(&lessons);
1268 assert!(!uses_lx(&load(&lessons)));
1269 same(&lessons, &cs, &load(&lessons), "rebuilt");
1270 let _ = std::fs::remove_dir_all(d);
1271 }
1272
1273 #[test]
1276 fn a_warm_load_parses_only_the_appended_lines() {
1277 let d = dir("warm");
1278 let lessons = d.join("lessons.jsonl");
1279 let (pl, pt) = (outcome::path_beside(&lessons), d.join(outcome::TEAM_FILE));
1280 let (local, team, cs) = (0..40).map(logs).find(|(l, t, _)| l.contains("\"regretted\"") && t.contains("\"regretted\"")).expect("a store with regrets");
1281 std::fs::write(&pl, &local).unwrap();
1282 std::fs::write(&pt, &team).unwrap();
1283 let count = || (PARSED.with(|c| c.replace(0)), REGRET_READS.with(|c| c.replace(0)));
1284 count();
1285 let cold = load(&lessons);
1286 assert_eq!(count(), (local.lines().count() + team.lines().count(), 0), "cold: every line, no regrets file");
1287 assert!(cold.generation != 0 && regrets_beside(&lessons).exists());
1288 assert!(load(&lessons) == cold);
1289 assert_eq!(count(), (0, 0), "warm: nothing parsed, no regrets file read");
1290 let line = |kind: &str, session: &str| {
1291 let o = Outcome {
1292 kind: kind.into(),
1293 session: session.into(),
1294 lessons: vec!["L1".into()],
1295 ms: 99,
1296 cause: "R9".into(),
1297 credit: 0.5,
1298 ..Default::default()
1299 };
1300 serde_json::to_string(&o).unwrap() + "\n"
1301 };
1302 use std::io::Write;
1303 let append = |text: &str| std::fs::OpenOptions::new().append(true).open(&pl).unwrap().write_all(text.as_bytes()).unwrap();
1304 append(&line(outcome::CHANGED, "s1"));
1305 same(&lessons, &cs, &load(&lessons), "a change appended");
1306 assert_eq!(count(), (1, 0), "a change: its line, no regrets file");
1307 append(&line(outcome::REGRETTED, "s1"));
1308 same(&lessons, &cs, &load(&lessons), "a regret appended");
1309 assert_eq!(count(), (1, 1), "a regret: its line and the regrets file");
1310 same(&lessons, &cs, &load(&lessons), "warm again");
1311 assert_eq!(count(), (0, 0));
1312 let _ = std::fs::remove_dir_all(d);
1313 }
1314
1315 #[test]
1318 fn a_team_session_that_turns_out_to_be_mine_leaves_the_view() {
1319 let d = dir("mine");
1320 let lessons = d.join("lessons.jsonl");
1321 let (pl, pt) = (outcome::path_beside(&lessons), d.join(outcome::TEAM_FILE));
1322 let cs = logs(0).2;
1323 let o = |kind: &str, session: &str, ms: i64| Outcome {
1324 kind: kind.into(),
1325 session: session.into(),
1326 lessons: vec!["L1".into()],
1327 ms,
1328 repo: "repoA".into(),
1329 cause: if kind == outcome::REGRETTED { "R1".into() } else { String::new() },
1330 credit: 0.75,
1331 ..Default::default()
1332 };
1333 let shared = [o(outcome::CHANGED, "me", 3), o(outcome::REGRETTED, "me", 4)];
1334 let team: String = shared.iter().map(|x| serde_json::to_string(&outcome::share(x, "this-node").unwrap()).unwrap() + "\n").collect();
1335 std::fs::write(&pt, &team).unwrap();
1336 std::fs::write(&pl, serde_json::to_string(&o(outcome::ASKED, "other", 1)).unwrap() + "\n").unwrap();
1337 let before = load(&lessons);
1338 same(&lessons, &cs, &before, "a peer's");
1339 assert!(!before.view(true).folded.regret.is_empty());
1340 let mine: String = shared.iter().map(|x| serde_json::to_string(x).unwrap() + "\n").collect();
1341 use std::io::Write;
1342 std::fs::OpenOptions::new().append(true).open(&pl).unwrap().write_all(mine.as_bytes()).unwrap();
1343 let after = load(&lessons);
1344 same(&lessons, &cs, &after, "mine");
1345 assert!(after.view(true).folded.regret.iter().all(|(_, s, _)| *s == "me"), "only this machine's own regret");
1346 let _ = std::fs::remove_dir_all(d);
1347 }
1348
1349 #[test]
1351 fn a_tick_at_the_largest_ms_is_a_tick() {
1352 let d = dir("max");
1353 let lessons = d.join("lessons.jsonl");
1354 let o = |kind: &str, session: &str, lesson: &str, ms: i64| Outcome {
1355 kind: kind.into(),
1356 session: session.into(),
1357 lessons: vec![lesson.into()],
1358 ms,
1359 ..Default::default()
1360 };
1361 let log = [o(outcome::CHANGED, "s1", "L1", 10), o(outcome::ASKED, "s2", "L2", i64::MAX), o(outcome::CHANGED, "s3", "L1", 20)];
1362 std::fs::write(outcome::path_beside(&lessons), log.iter().map(|x| serde_json::to_string(x).unwrap() + "\n").collect::<String>()).unwrap();
1363 let cs: Vec<Challenge> = ["L1", "L2"].iter().map(|id| Challenge { id: id.to_string(), origin_ms: 1, ..Default::default() }).collect();
1364 same(&lessons, &cs, &load(&lessons), "i64::MAX");
1365 assert_eq!(load(&lessons).view(false).folded.clock.now(None), 3, "three sessions tick");
1366 let _ = std::fs::remove_dir_all(d);
1367 }
1368
1369 #[test]
1372 fn another_version_or_generation_folds_afresh() {
1373 let d = dir("version");
1374 let lessons = d.join("lessons.jsonl");
1375 let (pl, pt) = (outcome::path_beside(&lessons), d.join(outcome::TEAM_FILE));
1376 let (local, team, cs) = (0..20).map(logs).find(|(l, _, _)| l.contains("\"regretted\"")).expect("a store with regrets");
1377 std::fs::write(&pl, &local).unwrap();
1378 std::fs::write(&pt, &team).unwrap();
1379 let cold = load(&lessons);
1380 let other = |b: &[u8]| {
1382 let mut body = b[..b.len() - 32].to_vec();
1383 body[8] ^= 1;
1384 W(body).seal()
1385 };
1386 let snap = std::fs::read(path_beside(&lessons)).unwrap();
1387 assert!(decode(&snap).is_some() && decode(&other(&snap)).is_none());
1388 let regrets = std::fs::read(regrets_beside(&lessons)).unwrap();
1389 let n = cold.strings.len();
1390 assert!(decode_regrets(®rets, cold.generation, n).is_some() && decode_regrets(&other(®rets), cold.generation, n).is_none());
1391 std::fs::write(path_beside(&lessons), other(&snap)).unwrap();
1392 PARSED.with(|c| c.set(0));
1393 same(&lessons, &cs, &load(&lessons), "other rules");
1394 assert_eq!(PARSED.with(|c| c.replace(0)), local.lines().count() + team.lines().count(), "folded afresh");
1395 let saved = decode(&std::fs::read(path_beside(&lessons)).unwrap()).unwrap();
1397 let mut theirs = saved.clone();
1398 theirs.regrets_file = Some(regrets_beside(&lessons));
1399 let r = theirs.regrets().cloned().unwrap();
1400 std::fs::write(regrets_beside(&lessons), encode_regrets(&r, saved.generation ^ 1)).unwrap();
1401 let regret = local.lines().find(|l| l.contains("\"regretted\"")).unwrap();
1402 use std::io::Write;
1403 std::fs::OpenOptions::new().append(true).open(&pl).unwrap().write_all(format!("{regret}\n").as_bytes()).unwrap();
1404 same(&lessons, &cs, &load(&lessons), "other generation");
1405 assert_eq!(PARSED.with(|c| c.replace(0)), 1 + local.lines().count() + 1 + team.lines().count(), "its line, then all afresh");
1406 same(&lessons, &cs, &load(&lessons), "warm after");
1407 let _ = std::fs::remove_dir_all(d);
1408 }
1409
1410 #[test]
1412 fn no_log_no_snapshot() {
1413 let d = dir("none");
1414 let lessons = d.join("lessons.jsonl");
1415 assert_eq!(load(&lessons), Facts::default());
1416 rebuild(&lessons);
1417 assert!(!path_beside(&lessons).exists());
1418 let _ = std::fs::remove_dir_all(d);
1419 }
1420
1421 #[test]
1425 #[ignore]
1426 fn the_facts_equal_a_fresh_derive_on_a_real_store() {
1427 let dir = PathBuf::from(std::env::var_os("BORG_FACTS_STORE").expect("set BORG_FACTS_STORE"));
1428 let lessons = dir.join("lessons.jsonl");
1429 let cs = crate::conscience::load_store_unweighted(&lessons, None);
1430 let sessions = std::env::var("BORG_FACTS_SESSIONS").unwrap_or_default();
1431 let t = std::time::Instant::now();
1432 let cold = load(&lessons);
1433 eprintln!("cold {:?}", t.elapsed());
1434 let t = std::time::Instant::now();
1435 let warm = load(&lessons);
1436 eprintln!("warm {:?}, {} bytes", t.elapsed(), encode(&warm).len());
1437 assert_eq!(decode(&encode(&warm)).as_ref(), Some(&warm));
1438 for on in [true, false] {
1439 let outcomes = outcome::team_view(&lessons, on);
1440 for session in sessions.split(',').filter(|s| !s.is_empty()).map(Some).chain([None, Some("a-new-session")]) {
1441 for facts in [&cold, &warm] {
1442 let want = memory::derive(&outcomes, &cs, session);
1443 let got = memory::derive_folded(facts.view(on).folded, &cs, session);
1444 assert_eq!(want.len(), got.len());
1445 for (id, a) in &want {
1446 let b = &got[id];
1447 assert_eq!(
1448 (a.changed, a.ignored, a.corrected, a.vetoed, a.tested, a.consolidated, a.dormant),
1449 (b.changed, b.ignored, b.corrected, b.vetoed, b.tested, b.consolidated, b.dormant),
1450 "{id}"
1451 );
1452 assert!((a.activation - b.activation).abs() < 1e-9 && (a.regretted - b.regretted).abs() < 1e-9, "{id}: {a:?} != {b:?}");
1453 }
1454 }
1455 eprintln!("on={on} {session:?}: {} lessons equal", cs.len());
1456 }
1457 }
1458 same(&lessons, &cs, &warm, "real store");
1459 }
1460}