1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
//! Learned immunity: persistent heal memory for supervised programs.
//!
//! `axiom run` heals a program's environment reactively — it waits for the
//! crash, repairs, and restarts. This module adds the *acquired* layer: every
//! successful heal is remembered against a stable fingerprint of the command,
//! together with the tension (CE) profile of its past failures. The next time
//! the same program runs — even in a fresh environment — the supervisor
//! **immunizes** it first: remembered directories are re-created *before* the
//! first attempt, so the failure never happens at all.
//!
//! The memory also keeps a running mean of failure-trace CE per program, so a
//! new failure can be classified as a KNOWN failure mode (tension close to the
//! historical mean) or a NOVEL one (a genuine drift spike). The classification
//! is informational with an unbaked checkpoint and sharp with the trained
//! semantic model — the plumbing is identical.
//!
//! Storage is a small human-readable JSON file (default
//! `~/.axiom/heal_memory.json`). Only directory heals are remembered: transient
//! retries are situational and never replayed prophylactically.
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::belief::BetaBelief;
/// Current unix time in seconds (0 if the clock is before the epoch).
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
/// What the memory knows about one supervised program.
///
/// The confidence fields model an adaptive-immunity lifecycle: a freshly
/// learned heal is *tentative*; each time immunizing the program produces a
/// successful run the confidence matures toward 1.0 (affinity maturation);
/// and confidence wanes with time since last reinforcement (memory waning),
/// so heals that are never exercised again fade and can be pruned.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ProgramRecord {
/// Human-readable command line (for inspection of the JSON; the map key is
/// the fingerprint).
pub command: String,
/// Directories this program needed created in the past.
pub dirs: Vec<PathBuf>,
/// Environment variables this program reported as required-but-unset. A
/// value cannot be safely fabricated, so these are surfaced as diagnostics
/// (in advisories / `axiom immunity`) rather than auto-applied.
#[serde(default)]
pub required_env: Vec<String>,
/// Running mean of failure-trace cross-entropy.
pub ce_mean: f32,
/// Number of failures folded into `ce_mean`.
pub ce_count: u32,
/// Back-compat scalar mirror of `belief.mean()` (kept so older readers and
/// JSON inspection still see a confidence number). The Beta belief is the
/// source of truth.
#[serde(default)]
pub confidence: f32,
/// Times immunizing this program preceded a successful run.
#[serde(default)]
pub immunizations: u32,
/// Unix seconds of the last confidence reinforcement (for waning).
#[serde(default)]
pub last_reinforced: u64,
/// Beta-distribution belief (estimate + uncertainty) — the source of truth
/// for confidence. `None` on legacy records; migrated from the scalar on
/// first access.
#[serde(default)]
pub belief: Option<BetaBelief>,
}
/// Half-life (days) of unreinforced immunity confidence.
const CONFIDENCE_HALFLIFE_DAYS: f32 = 30.0;
/// A belief whose time-decay has pushed its variance to/above this (near the
/// uniform prior's 0.083) has lost its evidence and is forgettable.
const FADED_VARIANCE: f32 = 0.07;
/// Cap on a peer-reported belief's total pseudocount mass — beyond this it is
/// treated as fabricated certainty (byzantine) and rejected from a merge.
const MAX_PEER_EVIDENCE: f32 = 1.0e4;
impl ProgramRecord {
/// The stored Beta belief, migrating a legacy scalar-confidence record into
/// a Beta on first access (evidence ≈ the immunization count).
fn base_belief(&self) -> BetaBelief {
self.belief.unwrap_or_else(|| {
if self.confidence > 0.0 {
BetaBelief::from_confidence(self.confidence, (self.immunizations + 2) as f32)
} else {
BetaBelief::uniform()
}
})
}
/// Time-decay factor in [0,1] since the last reinforcement (0 = none).
fn decay_factor(&self, now: u64) -> f32 {
if self.last_reinforced == 0 {
return 0.0;
}
let age_days = now.saturating_sub(self.last_reinforced) as f32 / 86_400.0;
1.0 - 0.5f32.powf(age_days / CONFIDENCE_HALFLIFE_DAYS)
}
/// The belief regressed toward the uniform prior by its age (waning).
pub fn belief_now(&self, now: u64) -> BetaBelief {
self.base_belief().decayed(self.decay_factor(now))
}
/// Decayed mean — the scalar confidence, for callers that want a number.
pub fn confidence_now(&self, now: u64) -> f32 {
self.belief_now(now).mean()
}
/// A human label for a decayed-confidence value.
pub fn confidence_label(c: f32) -> &'static str {
if c >= 0.85 {
"established"
} else if c >= 0.6 {
"proven"
} else if c >= 0.3 {
"tentative"
} else {
"faded"
}
}
}
/// Classification of a fresh failure against the program's tension history.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Novelty {
/// First observed failure — nothing to compare against.
First,
/// Tension within tolerance of the historical mean: a known failure mode.
Known,
/// Tension deviates from the historical mean: a new failure mode.
Novel,
}
impl std::fmt::Display for Novelty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Novelty::First => write!(f, "FIRST"),
Novelty::Known => write!(f, "KNOWN"),
Novelty::Novel => write!(f, "NOVEL"),
}
}
}
/// Relative CE deviation below which a failure counts as a known mode.
const KNOWN_TOLERANCE: f32 = 0.05;
#[derive(Debug, Default, Serialize, Deserialize)]
struct MemoryFile {
programs: HashMap<String, ProgramRecord>,
}
/// What a swarm-immunity merge changed locally.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct MergeReport {
/// Programs we had never seen, adopted from the peer.
pub programs_added: usize,
/// Programs both sides knew, whose records were combined.
pub programs_merged: usize,
/// New remembered directories gained from the peer.
pub dirs_added: usize,
/// Programs whose belief was irreconcilable under Dempster-Shafer (peer and
/// local strongly disagreed) — the local belief was kept, peer's rejected.
pub belief_conflicts: usize,
/// Peer records rejected as byzantine (implausible belief parameters).
pub byzantine_rejected: usize,
}
/// Persistent heal memory, loaded eagerly and saved explicitly.
#[derive(Debug)]
pub struct HealMemory {
path: PathBuf,
data: MemoryFile,
}
/// Shell wrappers whose raw `-c` snippets are too generic to advise on.
const SHELL_WRAPPERS: [&str; 9] = [
"sh", "bash", "zsh", "dash", "fish", "cmd", "cmd.exe", "powershell", "pwsh",
];
/// A lowercase, matchable signature for a command line — the program name plus
/// its first non-flag argument (e.g. "cargo build", "pytest", "npm test").
/// Returns `None` for shell wrappers and trivially short signatures, so
/// advisory matching stays precise (no false positives on prose).
pub fn command_signature(command_line: &str) -> Option<String> {
let tokens: Vec<&str> = command_line.split_whitespace().collect();
let first = tokens.first()?;
if SHELL_WRAPPERS.contains(&first.to_ascii_lowercase().as_str()) {
return None;
}
// program + first argument that isn't a flag or a redirection/path.
let mut sig: Vec<String> = vec![first.to_ascii_lowercase()];
if let Some(arg) = tokens.iter().skip(1).find(|t| {
!t.starts_with('-') && !t.starts_with('/') && !t.contains('>') && !t.contains('<')
}) {
sig.push(arg.to_ascii_lowercase());
}
let joined = sig.join(" ");
(joined.len() >= 3).then_some(joined)
}
/// Stable fingerprint of a supervised command line.
pub fn fingerprint(command: &str, args: &[String]) -> String {
let mut hasher = Sha256::new();
hasher.update(command.as_bytes());
for a in args {
hasher.update([0u8]);
hasher.update(a.as_bytes());
}
format!("{:x}", hasher.finalize())
}
/// Express `dir` relative to `anchor` when it lives under it (making the heal
/// portable across checkouts/machines); otherwise return `dir` unchanged so
/// out-of-tree absolute heals are preserved verbatim.
pub fn relativize_dir(dir: &Path, anchor: &Path) -> PathBuf {
match dir.strip_prefix(anchor) {
Ok(rel) if rel.as_os_str().is_empty() => PathBuf::from("."),
Ok(rel) => rel.to_path_buf(),
Err(_) => dir.to_path_buf(),
}
}
/// Inverse of [`relativize_dir`]: resolve a stored heal against `anchor`. A
/// relative heal is anchored here (portable immunity); an absolute heal is used
/// verbatim.
pub fn resolve_dir(dir: &Path, anchor: &Path) -> PathBuf {
if dir.is_relative() {
anchor.join(dir)
} else {
dir.to_path_buf()
}
}
impl HealMemory {
/// Load the memory at `path`, or start empty when missing/corrupt (a bad
/// memory file must never break a run).
pub fn load(path: impl AsRef<Path>) -> Self {
let path = path.as_ref().to_path_buf();
let data = std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
Self { path, data }
}
pub fn record(&self, fp: &str) -> Option<&ProgramRecord> {
self.data.programs.get(fp)
}
/// Every program this memory knows about, newest-insertion-order-agnostic.
pub fn all_records(&self) -> Vec<&ProgramRecord> {
self.data.programs.values().collect()
}
/// Records whose command line contains `query` (case-insensitive). An empty
/// query matches everything.
pub fn find(&self, query: &str) -> Vec<&ProgramRecord> {
let q = query.trim().to_ascii_lowercase();
self.data
.programs
.values()
.filter(|r| q.is_empty() || r.command.to_ascii_lowercase().contains(&q))
.collect()
}
/// Human- and agent-readable summary of acquired immunity. `query` filters
/// by command substring; `None`/empty lists everything.
pub fn report_text(&self, query: Option<&str>) -> String {
let mut records = self.find(query.unwrap_or(""));
if records.is_empty() {
return match query {
Some(q) if !q.trim().is_empty() => {
format!("Axiom has no acquired immunity matching \"{q}\".")
}
_ => "Axiom has not learned any program failures yet.".to_string(),
};
}
// Stable output: most-experienced programs first.
records.sort_by(|a, b| b.ce_count.cmp(&a.ce_count).then(a.command.cmp(&b.command)));
let now = now_secs();
let mut out = format!("Acquired immunity ({} program(s)):\n", records.len());
for r in records {
let belief = r.belief_now(now);
out.push_str(&format!("\n• {}\n", r.command));
out.push_str(&format!(
" confidence: {:.2} ±{:.2} ({}, immunizations: {}) failures observed: {} mean tension (CE): {:.3}\n",
belief.mean(),
belief.variance().sqrt(),
belief.label(),
r.immunizations,
r.ce_count,
r.ce_mean
));
if r.dirs.is_empty() {
out.push_str(" learned heals: none (no directory heals)\n");
} else {
out.push_str(" learned heals: pre-create directories\n");
for d in &r.dirs {
out.push_str(&format!(" - {}\n", d.display()));
}
}
if !r.required_env.is_empty() {
out.push_str(&format!(
" requires env vars (set before running): {}\n",
r.required_env.join(", ")
));
}
}
out
}
/// Advisory lines for any learned program whose command is referenced in
/// `text`. Only programs with concrete heals (directory pre-creation) and a
/// non-generic command signature qualify — so this never fires on bare
/// shell snippets, and a match means Axiom genuinely knows a fix.
pub fn advisories_for_text(&self, text: &str) -> Vec<String> {
let haystack = text.to_ascii_lowercase();
let mut out = Vec::new();
let mut records: Vec<&ProgramRecord> = self
.data
.programs
.values()
.filter(|r| !r.dirs.is_empty() || !r.required_env.is_empty())
.collect();
records.sort_by(|a, b| a.command.cmp(&b.command));
for r in records {
let Some(sig) = command_signature(&r.command) else {
continue;
};
if !haystack.contains(&sig) {
continue;
}
if !r.dirs.is_empty() {
let dirs = r
.dirs
.iter()
.map(|d| d.display().to_string())
.collect::<Vec<_>>()
.join(", ");
// Phrase by the Beta belief: an *established* fix (high mean AND
// low variance — genuinely settled evidence) is asserted; a
// high-mean-but-uncertain or low-mean fix is merely offered.
let preamble = if r.belief_now(now_secs()).is_established() {
format!(
"`{}` reliably fails in this environment (fixed {} time(s)); Axiom's established fix",
r.command, r.immunizations
)
} else {
format!(
"`{}` has failed in this environment before; Axiom's tentative fix",
r.command
)
};
out.push(format!(
"{preamble}: create director{} {}. Apply preemptively if it fails again.",
if r.dirs.len() == 1 { "y" } else { "ies" },
dirs
));
}
if !r.required_env.is_empty() {
out.push(format!(
"`{}` requires environment variable(s) {} to be set — Axiom cannot \
fabricate the value(s); ensure they are exported.",
r.command,
r.required_env.join(", ")
));
}
}
out
}
/// Cross-reactive immunity: analogical hints from *sibling* commands in the
/// same program family (same executable, different sub-command) that carry
/// learned heals. Fires when the program name is referenced in `text` but
/// the sibling's exact signature is not — generalizing immunity by analogy.
/// These are advisory ONLY (never auto-applied), so a wrong analogy costs
/// nothing. Capped to keep proxy payloads lean.
pub fn cross_reactive_hints(&self, text: &str) -> Vec<String> {
let hay = text.to_ascii_lowercase();
let mut out = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut records: Vec<&ProgramRecord> = self
.data
.programs
.values()
.filter(|r| !r.dirs.is_empty())
.collect();
records.sort_by(|a, b| a.command.cmp(&b.command));
for r in records {
let Some(sig) = command_signature(&r.command) else {
continue;
};
// Cross-reactivity is for multi-sub-command families ("cargo build"),
// not single-command tools, and the exact command must NOT already be
// referenced (that path is a direct advisory, not an analogy).
let mut parts = sig.splitn(2, ' ');
let prog = parts.next().unwrap_or("");
if parts.next().is_none() || prog.len() < 3 || hay.contains(&sig) {
continue;
}
if !hay.contains(prog) || !seen.insert(format!("{prog}:{:?}", r.dirs)) {
continue;
}
let dirs = r
.dirs
.iter()
.map(|d| d.display().to_string())
.collect::<Vec<_>>()
.join(", ");
out.push(format!(
"cross-reactive hint: a sibling `{}` previously needed director{} {}; \
a different `{prog} …` invocation here may need the same (Axiom has \
not applied it).",
r.command,
if r.dirs.len() == 1 { "y" } else { "ies" },
dirs
));
if out.len() >= 3 {
break;
}
}
out
}
/// Resolve the default heal-memory path used across the engine:
/// `AXIOM_HEAL_MEMORY` overrides it; `0`/`off` disables it (returns `None`);
/// otherwise `~/.axiom/heal_memory.json`.
pub fn default_path() -> Option<PathBuf> {
match std::env::var("AXIOM_HEAL_MEMORY") {
Ok(v) if v == "0" || v.eq_ignore_ascii_case("off") => None,
Ok(v) => Some(PathBuf::from(v)),
Err(_) => dirs::home_dir().map(|h| h.join(".axiom").join("heal_memory.json")),
}
}
/// Re-create every remembered directory that is missing. Returns the dirs
/// actually created now (the immunization applied to *this* environment).
pub fn immunize(&self, fp: &str) -> Vec<PathBuf> {
let anchor = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
self.immunize_anchored(fp, &anchor)
}
/// Re-create remembered-but-missing directories, re-anchoring *relative*
/// heals to `anchor` (the supervised process's working directory). This is
/// what makes immunity location-invariant: a heal learned as `target` in
/// one checkout is applied as `<anchor>/target` here. Absolute heals are
/// used verbatim. Returns the resolved absolute paths actually created.
pub fn immunize_anchored(&self, fp: &str, anchor: &Path) -> Vec<PathBuf> {
let Some(record) = self.data.programs.get(fp) else {
return Vec::new();
};
let mut applied = Vec::new();
for dir in &record.dirs {
let resolved = resolve_dir(dir, anchor);
if !resolved.exists() && std::fs::create_dir_all(&resolved).is_ok() {
applied.push(resolved);
}
}
applied
}
/// Anticipatory immunity: the program's learned prerequisite directories
/// that are *currently missing* (re-anchored). A non-empty result predicts
/// the program would fail right now for want of those directories — before
/// it is ever run. Returns `(resolved_missing_dirs, confidence)`; an unknown
/// program yields an empty list.
pub fn missing_prerequisites(&self, fp: &str, anchor: &Path) -> (Vec<PathBuf>, f32) {
let Some(record) = self.data.programs.get(fp) else {
return (Vec::new(), 0.0);
};
let missing = record
.dirs
.iter()
.map(|d| resolve_dir(d, anchor))
.filter(|d| !d.exists())
.collect();
(missing, record.confidence_now(now_secs()))
}
/// Fold one failure's tension into the program's CE history and classify it
/// against what came before.
pub fn observe_failure(&mut self, fp: &str, command_line: &str, ce: f32) -> Novelty {
let record = self
.data
.programs
.entry(fp.to_string())
.or_insert_with(|| ProgramRecord {
command: command_line.to_string(),
..ProgramRecord::default()
});
let novelty = if record.ce_count == 0 {
Novelty::First
} else if !ce.is_finite() || record.ce_mean == 0.0 {
Novelty::Novel
} else if ((ce - record.ce_mean) / record.ce_mean).abs() <= KNOWN_TOLERANCE {
Novelty::Known
} else {
Novelty::Novel
};
if ce.is_finite() {
let n = record.ce_count as f32;
record.ce_mean = (record.ce_mean * n + ce) / (n + 1.0);
record.ce_count += 1;
}
novelty
}
/// Remember directories a successful run needed. Deduplicates.
pub fn remember_dirs(&mut self, fp: &str, command_line: &str, dirs: &[PathBuf]) {
if dirs.is_empty() {
return;
}
let record = self
.data
.programs
.entry(fp.to_string())
.or_insert_with(|| ProgramRecord {
command: command_line.to_string(),
..ProgramRecord::default()
});
for d in dirs {
if !record.dirs.contains(d) {
record.dirs.push(d.clone());
}
}
// A freshly learned heal is tentative: the uniform prior Beta(1,1) —
// mean 0.5, maximum uncertainty — until reuse accumulates evidence.
if record.belief.is_none() {
let b = BetaBelief::uniform();
record.confidence = b.mean();
record.belief = Some(b);
}
}
/// Record environment variables a program reported as required-but-unset, so
/// they can be surfaced as diagnostics. Deduplicates; creates the record if
/// this is the program's first observation.
pub fn remember_env_requirement(&mut self, fp: &str, command_line: &str, vars: &[String]) {
if vars.is_empty() {
return;
}
let record = self
.data
.programs
.entry(fp.to_string())
.or_insert_with(|| ProgramRecord {
command: command_line.to_string(),
..ProgramRecord::default()
});
for v in vars {
if !record.required_env.contains(v) {
record.required_env.push(v.clone());
}
}
}
/// Reinforce a program's immunity after immunizing it preceded a successful
/// run — affinity maturation: add a success pseudocount to the Beta belief
/// (raising the mean, lowering variance) and reset the waning clock. The
/// belief is first decayed to `now` so reinforcement after a long gap builds
/// on a faded (re-uncertain) memory rather than over-crediting it.
pub fn reinforce_immunity(&mut self, fp: &str, now: u64) {
if let Some(r) = self.data.programs.get_mut(fp) {
let mut b = r.belief_now(now);
b.reinforce();
r.confidence = b.mean();
r.belief = Some(b);
r.immunizations += 1;
r.last_reinforced = now;
}
}
/// Forget records whose belief has decayed back to near-uniform uncertainty
/// (its evidence lost to waning → clonal deletion). Returns the number
/// forgotten. Never-reinforced heals are kept (they have not had a chance to
/// mature or fade).
pub fn prune_stale(&mut self, now: u64) -> usize {
let before = self.data.programs.len();
self.data
.programs
.retain(|_, r| r.last_reinforced == 0 || r.belief_now(now).variance() < FADED_VARIANCE);
before - self.data.programs.len()
}
/// Serialize the whole memory for transfer to a swarm peer.
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(&self.data).unwrap_or_else(|_| "{}".into())
}
/// Merge a peer's exported memory into this one (swarm immunity), with
/// epistemic + byzantine-resistant trust rules:
///
/// * **Byzantine gate** — a peer record whose belief parameters are
/// implausible (NaN/∞/negative, or fabricated-certainty pseudocounts) is
/// rejected outright (`byzantine_rejected`).
/// * **Dempster-Shafer combination** — for a program both sides know, the
/// two Beta beliefs are combined via DS evidence combination: agreeing
/// peers *compound* evidence (mean up, variance down); irreconcilable
/// peers raise a conflict, the local belief is kept, and the peer's is
/// rejected (`belief_conflicts`) rather than silently averaged.
/// * Directory lists are unioned; failure-tension is count-weighted-mean.
///
/// A malformed peer payload is rejected without touching local state.
pub fn merge_json(&mut self, peer_json: &str) -> Result<MergeReport, String> {
let peer: MemoryFile =
serde_json::from_str(peer_json).map_err(|e| format!("invalid peer memory: {e}"))?;
let mut report = MergeReport::default();
for (fp, theirs) in peer.programs {
// Byzantine gate: a peer cannot inject malformed/fabricated beliefs.
if let Some(pb) = theirs.belief {
if !pb.is_plausible(MAX_PEER_EVIDENCE) {
report.byzantine_rejected += 1;
continue;
}
}
match self.data.programs.get_mut(&fp) {
None => {
report.programs_added += 1;
report.dirs_added += theirs.dirs.len();
self.data.programs.insert(fp, theirs);
}
Some(ours) => {
report.programs_merged += 1;
for d in &theirs.dirs {
if !ours.dirs.contains(d) {
ours.dirs.push(d.clone());
report.dirs_added += 1;
}
}
let total = ours.ce_count + theirs.ce_count;
if total > 0 {
ours.ce_mean = (ours.ce_mean * ours.ce_count as f32
+ theirs.ce_mean * theirs.ce_count as f32)
/ total as f32;
ours.ce_count = total;
}
// Dempster-Shafer belief combination with conflict detection.
match ours.base_belief().combine_ds(&theirs.base_belief()) {
Ok(combined) => {
ours.confidence = combined.mean();
ours.belief = Some(combined);
ours.immunizations += theirs.immunizations;
ours.last_reinforced =
ours.last_reinforced.max(theirs.last_reinforced);
}
Err(_conflict) => {
// Irreconcilable: keep our belief, reject theirs.
report.belief_conflicts += 1;
}
}
}
}
}
Ok(report)
}
/// Persist to disk (pretty JSON so the memory stays human-auditable).
pub fn save(&self) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&self.data)
.map_err(|e| std::io::Error::other(e.to_string()))?;
std::fs::write(&self.path, json)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("axiom_healmem_{tag}_{nanos}.json"))
}
#[test]
fn fingerprint_is_stable_and_arg_sensitive() {
let a = fingerprint("sh", &["-c".into(), "x".into()]);
let b = fingerprint("sh", &["-c".into(), "x".into()]);
let c = fingerprint("sh", &["-c".into(), "y".into()]);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn remember_save_load_immunize_roundtrip() {
let mem_path = tmp("roundtrip");
let dir = std::env::temp_dir().join(tmp("dir").file_stem().unwrap().to_os_string());
let fp = fingerprint("prog", &[]);
let mut mem = HealMemory::load(&mem_path);
mem.remember_dirs(&fp, "prog", &[dir.clone()]);
mem.save().unwrap();
// Fresh load in a "fresh environment" (dir does not exist yet).
assert!(!dir.exists());
let mem2 = HealMemory::load(&mem_path);
let applied = mem2.immunize(&fp);
assert_eq!(applied, vec![dir.clone()]);
assert!(dir.exists(), "immunization must re-create the remembered dir");
// Second immunization is a no-op (already present).
assert!(mem2.immunize(&fp).is_empty());
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&mem_path);
}
#[test]
fn novelty_classification_tracks_the_mean() {
let mut mem = HealMemory::load(tmp("novelty"));
let fp = fingerprint("prog", &[]);
assert_eq!(mem.observe_failure(&fp, "prog", 5.0), Novelty::First);
assert_eq!(mem.observe_failure(&fp, "prog", 5.01), Novelty::Known);
assert_eq!(mem.observe_failure(&fp, "prog", 9.0), Novelty::Novel);
}
#[test]
fn merge_adopts_unknown_and_unions_known_programs() {
let fp_shared = fingerprint("shared", &[]);
let fp_peer_only = fingerprint("peer-only", &[]);
let mut ours = HealMemory::load(tmp("merge_ours"));
ours.remember_dirs(&fp_shared, "shared", &[PathBuf::from("/a")]);
ours.observe_failure(&fp_shared, "shared", 4.0); // count=1, mean=4.0
let mut theirs = HealMemory::load(tmp("merge_theirs"));
theirs.remember_dirs(&fp_shared, "shared", &[PathBuf::from("/a"), PathBuf::from("/b")]);
// Three failures at 6.0 → their history outweighs ours 3:1.
for _ in 0..3 {
theirs.observe_failure(&fp_shared, "shared", 6.0);
}
theirs.remember_dirs(&fp_peer_only, "peer-only", &[PathBuf::from("/c")]);
let report = ours.merge_json(&theirs.to_json()).unwrap();
assert_eq!(report.programs_added, 1);
assert_eq!(report.programs_merged, 1);
assert_eq!(report.dirs_added, 2, "/b from shared + /c from peer-only");
let shared = ours.record(&fp_shared).unwrap();
assert_eq!(shared.dirs, vec![PathBuf::from("/a"), PathBuf::from("/b")]);
assert_eq!(shared.ce_count, 4);
assert!((shared.ce_mean - 5.5).abs() < 1e-5, "count-weighted: (4+18)/4");
assert!(ours.record(&fp_peer_only).is_some());
}
#[test]
fn merge_rejects_malformed_peer_payload() {
let fp = fingerprint("prog", &[]);
let mut ours = HealMemory::load(tmp("merge_bad"));
ours.remember_dirs(&fp, "prog", &[PathBuf::from("/keep")]);
assert!(ours.merge_json("{broken").is_err());
// Local state untouched.
assert_eq!(ours.record(&fp).unwrap().dirs, vec![PathBuf::from("/keep")]);
}
#[test]
fn merge_flags_dempster_shafer_conflict_and_keeps_local() {
let fp = fingerprint("prog", &[]);
let now = 1_000_000_000u64;
// Ours: strongly-established belief (many successes).
let mut ours = HealMemory::load(tmp("merge_conf_a"));
ours.remember_dirs(&fp, "prog", &[PathBuf::from("/a")]);
for k in 0..12 {
ours.reinforce_immunity(&fp, now + k);
}
let our_mean = ours.record(&fp).unwrap().belief_now(now + 12).mean();
// Theirs: a strongly-negative belief for the same program (penalized).
let mut theirs = HealMemory::load(tmp("merge_conf_b"));
theirs.remember_dirs(&fp, "prog", &[PathBuf::from("/a")]);
if let Some(r) = theirs.data.programs.get_mut(&fp) {
r.belief = Some(crate::belief::BetaBelief { alpha: 1.0, beta: 11.0 });
}
let report = ours.merge_json(&theirs.to_json()).unwrap();
assert_eq!(report.belief_conflicts, 1, "irreconcilable beliefs flagged");
// Local belief kept (not averaged into the peer's low value).
assert!((ours.record(&fp).unwrap().belief_now(now + 12).mean() - our_mean).abs() < 1e-3);
}
#[test]
fn merge_rejects_byzantine_fabricated_certainty() {
let fp = fingerprint("prog", &[]);
let mut ours = HealMemory::load(tmp("merge_byz_a"));
ours.remember_dirs(&fp, "prog", &[PathBuf::from("/a")]);
// Peer injects a fabricated-certainty belief (absurd pseudocount mass).
let mut theirs = HealMemory::load(tmp("merge_byz_b"));
theirs.remember_dirs(&fp, "prog", &[PathBuf::from("/evil")]);
if let Some(r) = theirs.data.programs.get_mut(&fp) {
r.belief = Some(crate::belief::BetaBelief { alpha: 1.0e9, beta: 1.0 });
}
let report = ours.merge_json(&theirs.to_json()).unwrap();
assert_eq!(report.byzantine_rejected, 1);
// The peer's directory must NOT have been merged in.
assert_eq!(ours.record(&fp).unwrap().dirs, vec![PathBuf::from("/a")]);
}
#[test]
fn report_text_summarizes_and_filters() {
let mut mem = HealMemory::load(tmp("report"));
let fp_cargo = fingerprint("cargo", &["build".into()]);
mem.remember_dirs(&fp_cargo, "cargo build", &[PathBuf::from("/target")]);
mem.observe_failure(&fp_cargo, "cargo build", 4.0);
let fp_py = fingerprint("python", &["app.py".into()]);
mem.observe_failure(&fp_py, "python app.py", 6.0);
let all = mem.report_text(None);
assert!(all.contains("cargo build") && all.contains("python app.py"));
assert!(all.contains("/target"), "remembered heals must be listed");
let filtered = mem.report_text(Some("cargo"));
assert!(filtered.contains("cargo build"));
assert!(!filtered.contains("python app.py"), "filter must exclude non-matches");
let miss = mem.report_text(Some("rustc"));
assert!(miss.contains("no acquired immunity matching"));
}
#[test]
fn cross_reactive_hints_generalize_within_a_program_family() {
let mut mem = HealMemory::load(tmp("xreact"));
mem.remember_dirs(
&fingerprint("cargo", &["build".into()]),
"cargo build",
&[PathBuf::from("target")],
);
// A sibling sub-command (`cargo test`) referenced but never learned →
// analogical hint from `cargo build`.
let hints = mem.cross_reactive_hints("why does cargo test fail in CI?");
assert_eq!(hints.len(), 1);
assert!(hints[0].contains("cargo build") && hints[0].contains("target"));
// The exact command directly referenced → NOT a cross-reactive hint
// (that's a direct advisory's job).
assert!(mem.cross_reactive_hints("cargo build broke").is_empty());
// A different program family → no analogy.
assert!(mem.cross_reactive_hints("npm test failing").is_empty());
}
#[test]
fn env_requirement_surfaces_in_advisory_and_report() {
let mut mem = HealMemory::load(tmp("envadv"));
let fp = fingerprint("cargo", &["run".into()]);
mem.remember_env_requirement(&fp, "cargo run", &["DATABASE_URL".into()]);
let adv = mem.advisories_for_text("why does cargo run fail?");
assert!(
adv.iter().any(|a| a.contains("DATABASE_URL") && a.contains("environment variable")),
"env requirement must surface as an advisory: {adv:?}"
);
assert!(mem.report_text(None).contains("requires env vars"));
}
#[test]
fn report_text_empty_memory_is_friendly() {
let mem = HealMemory::load(tmp("empty_report"));
assert!(mem.report_text(None).contains("has not learned any program failures"));
}
#[test]
fn command_signature_skips_shells_and_keeps_real_tools() {
assert_eq!(command_signature("cargo build --release").as_deref(), Some("cargo build"));
assert_eq!(command_signature("pytest").as_deref(), Some("pytest"));
assert_eq!(command_signature("npm test").as_deref(), Some("npm test"));
// shell wrappers and their snippets are too generic to advise on
assert_eq!(command_signature("sh -c echo hi > /x/y"), None);
assert_eq!(command_signature("bash -c make"), None);
}
#[test]
fn advisories_fire_only_on_referenced_healable_commands() {
let mut mem = HealMemory::load(tmp("advis"));
let fp = fingerprint("cargo", &["build".into()]);
mem.remember_dirs(&fp, "cargo build", &[PathBuf::from("/repo/target")]);
// A program with no learned dirs must never produce an advisory.
let fp2 = fingerprint("pytest", &[]);
mem.observe_failure(&fp2, "pytest", 5.0);
let hit = mem.advisories_for_text("why does cargo build keep failing in CI?");
assert_eq!(hit.len(), 1);
assert!(hit[0].contains("cargo build") && hit[0].contains("/repo/target"));
// pytest is referenced but has no learned heal → no advisory.
assert!(mem.advisories_for_text("my pytest run is slow").is_empty());
// cargo not mentioned → no advisory.
assert!(mem.advisories_for_text("unrelated question about npm").is_empty());
}
#[test]
fn confidence_matures_with_reuse_and_wanes_with_time() {
let mut mem = HealMemory::load(tmp("conf"));
let fp = fingerprint("cargo", &["build".into()]);
let now = 1_000_000_000u64;
// Freshly learned → tentative.
mem.remember_dirs(&fp, "cargo build", &[PathBuf::from("target")]);
assert!((mem.record(&fp).unwrap().confidence - 0.5).abs() < 1e-6);
// Each successful reuse matures confidence toward 1.0 (monotone up).
let mut prev = mem.record(&fp).unwrap().confidence_now(now);
for k in 1..=3 {
mem.reinforce_immunity(&fp, now + k);
let c = mem.record(&fp).unwrap().confidence_now(now + k);
assert!(c > prev, "reuse {k} must raise confidence ({prev} -> {c})");
prev = c;
}
assert_eq!(mem.record(&fp).unwrap().immunizations, 3);
assert!(prev >= 0.6, "after 3 reuses confidence should be 'proven'+");
let rec = mem.record(&fp).unwrap();
let fresh = rec.belief_now(now + 3);
assert!(fresh.is_established(), "after 3 reuses the belief is established");
// Waning: long after the last reuse the belief regresses toward the
// uniform prior — staleness becomes *uncertainty* (higher variance,
// mean drifting back toward 0.5), not a confidence driven to zero.
let later = now + 3 + 4 * (CONFIDENCE_HALFLIFE_DAYS as u64) * 86_400;
let stale = rec.belief_now(later);
assert!(!stale.is_established(), "a waned belief is no longer established");
assert!(stale.variance() > fresh.variance(), "waning raises uncertainty");
assert!(stale.mean() < fresh.mean(), "mean regresses toward 0.5");
}
#[test]
fn prune_forgets_only_faded_reinforced_heals() {
let mut mem = HealMemory::load(tmp("prune"));
let now = 2_000_000_000u64;
// A reinforced-then-ancient heal → should be pruned.
let faded = fingerprint("old", &[]);
mem.remember_dirs(&faded, "old", &[PathBuf::from("a")]);
mem.reinforce_immunity(&faded, now - 3650 * 86_400); // ~10 years ago
// A never-reinforced heal → kept (hasn't had a chance to mature/fade).
let fresh = fingerprint("new", &[]);
mem.remember_dirs(&fresh, "new", &[PathBuf::from("b")]);
let pruned = mem.prune_stale(now);
assert_eq!(pruned, 1);
assert!(mem.record(&faded).is_none(), "ancient heal forgotten");
assert!(mem.record(&fresh).is_some(), "unreinforced heal retained");
}
#[test]
fn corrupt_memory_file_degrades_to_empty() {
let path = tmp("corrupt");
std::fs::write(&path, "{not json").unwrap();
let mem = HealMemory::load(&path);
assert!(mem.record("anything").is_none());
let _ = std::fs::remove_file(&path);
}
}