Every evidence row now records its derivation (direct, assertion, operator, external, prediction, consolidation, inference, reflection, dream, retrieval), its independent-source family (an actor, an integration, an operator, a prediction) and its root event or memory ids. Confidence groups evidence by family: the strongest primary item counts in full, each further primary item adds a quarter, capped at 1.5x, and derived items count only when the family has no primary member. Three derived re-expressions of one utterance therefore weigh exactly what the utterance weighs, while three independent families still add up. Producers (conversation, association, consolidation, predictions, metacognition, dreaming, operator and seed input, contradictions) supply their ancestry. Existing rows are marked legacy_unknown with family = source; nothing is invented. The belief page shows per-family contributions and the replay test checks that provenance is reconstructed identically in the sandbox. Migration 0017 is additive.
128 lines
7.3 KiB
Go
128 lines
7.3 KiB
Go
package beliefs
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/andydixon/noema/pkg/concepts"
|
||
)
|
||
|
||
func TestComputeIsMonotonicBoundedAndDiscountsSingleSource(t *testing.T) {
|
||
none := Compute(0.5, nil)
|
||
if none.Confidence != 0.5 {
|
||
t.Fatalf("no evidence should keep the prior, got %v", none.Confidence)
|
||
}
|
||
one := Compute(0.5, []Evidence{{Direction: "for", Kind: "event", RefID: "a", Source: "metrics", Weight: 0.8, Credibility: 0.9}})
|
||
two := Compute(0.5, []Evidence{{Direction: "for", Kind: "event", RefID: "a", Source: "metrics", Weight: 0.8, Credibility: 0.9}, {Direction: "for", Kind: "event", RefID: "b", Source: "metrics", Weight: 0.8, Credibility: 0.9}})
|
||
diverse := Compute(0.5, []Evidence{{Direction: "for", Kind: "event", RefID: "a", Source: "metrics", Weight: 0.8, Credibility: 0.9}, {Direction: "for", Kind: "event", RefID: "b", Source: "logs", Weight: 0.8, Credibility: 0.9}, {Direction: "for", Kind: "event", RefID: "c", Source: "operator", Weight: 0.8, Credibility: 0.9}})
|
||
if !(none.Confidence < one.Confidence && one.Confidence < two.Confidence && two.Confidence < diverse.Confidence) {
|
||
t.Fatalf("not monotonic: %v %v %v %v", none.Confidence, one.Confidence, two.Confidence, diverse.Confidence)
|
||
}
|
||
if one.DiversityScale != 0.6 || len(one.Notes) == 0 || diverse.DiversityScale != 1 {
|
||
t.Fatalf("diversity discount wrong: %+v / %+v", one, diverse)
|
||
}
|
||
huge := make([]Evidence, 50)
|
||
for i := range huge {
|
||
huge[i] = Evidence{Direction: "for", Kind: "event", RefID: string(rune('a' + i)), Source: "s" + string(rune('a'+i)), Weight: 1, Credibility: 1}
|
||
}
|
||
if Compute(0.5, huge).Confidence > MaxConfidence {
|
||
t.Fatal("confidence exceeded bound")
|
||
}
|
||
against := Compute(0.5, []Evidence{{Direction: "against", Kind: "event", RefID: "x", Weight: 1, Credibility: 1}})
|
||
if against.Confidence >= 0.5 {
|
||
t.Fatal("opposing evidence should lower confidence")
|
||
}
|
||
retracted := Compute(0.5, []Evidence{{Direction: "for", Kind: "event", RefID: "a", Weight: 1, Credibility: 1, Retracted: true}})
|
||
if retracted.Confidence != 0.5 {
|
||
t.Fatal("retracted evidence must not count")
|
||
}
|
||
}
|
||
|
||
func TestDeriveStatuses(t *testing.T) {
|
||
cases := []struct {
|
||
b Breakdown
|
||
e concepts.Epistemic
|
||
want Status
|
||
}{
|
||
{Compute(PriorFor(concepts.Hypothesis), nil), concepts.Hypothesis, StatusHypothesis},
|
||
{Compute(0.5, []Evidence{{Direction: "for", Source: "a", Weight: 0.5, Credibility: 0.9}}), concepts.Claim, StatusTentative},
|
||
{Compute(0.5, []Evidence{{Direction: "for", Source: "a", Weight: 1, Credibility: 1}, {Direction: "for", Source: "b", Weight: 1, Credibility: 1}, {Direction: "for", Source: "c", Weight: 1, Credibility: 1}}), concepts.Claim, StatusAccepted},
|
||
{Compute(0.5, []Evidence{{Direction: "for", Source: "a", Weight: 0.8, Credibility: 0.9}, {Direction: "for", Source: "b", Weight: 0.8, Credibility: 0.9}, {Direction: "against", Source: "c", Weight: 0.8, Credibility: 0.9}}), concepts.Claim, StatusContested},
|
||
{Compute(0.5, []Evidence{{Direction: "against", Source: "a", Weight: 1, Credibility: 1}, {Direction: "against", Source: "b", Weight: 1, Credibility: 1}}), concepts.Claim, StatusRejected},
|
||
}
|
||
for i, c := range cases {
|
||
if got := Derive(c.b, c.e); got != c.want {
|
||
t.Errorf("case %d: got %s want %s (conf %v for %v against %v)", i, got, c.want, c.b.Confidence, c.b.For, c.b.Against)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestContradicts(t *testing.T) {
|
||
if ok, _ := Contradicts("server-01 has an intermittent storage problem", "server-01 does not have an intermittent storage problem"); !ok {
|
||
t.Fatal("negated paraphrase not detected")
|
||
}
|
||
if ok, _ := Contradicts("server-01 has a storage problem", "server-02 has a network problem"); ok {
|
||
t.Fatal("unrelated propositions flagged")
|
||
}
|
||
if ok, _ := Contradicts("atlas is slow", "atlas is slow"); ok {
|
||
t.Fatal("identical propositions flagged")
|
||
}
|
||
}
|
||
|
||
// TestProvenanceGroupingPreventsInflation pins the family arithmetic:
|
||
// independent families add up; derived re-expressions of one source add
|
||
// nothing to it; repetition within a family is heavily discounted; legacy
|
||
// rows (family = source) behave as primary evidence.
|
||
func TestProvenanceGroupingPreventsInflation(t *testing.T) {
|
||
ev := func(fam, deriv string, w float64) Evidence {
|
||
return Evidence{Direction: "for", Kind: "event", RefID: fam + deriv, Source: fam, Family: fam, Derivation: deriv, Weight: w, Credibility: 1}
|
||
}
|
||
one := Compute(0.5, []Evidence{ev("actor:andy", DerivationAssertion, 0.6)})
|
||
threeIndependent := Compute(0.5, []Evidence{ev("actor:andy", DerivationAssertion, 0.6), ev("source:prometheus:prod", DerivationExternal, 0.6), ev("operator:admin", DerivationOperator, 0.6)})
|
||
threeDerived := Compute(0.5, []Evidence{
|
||
ev("actor:andy", DerivationAssertion, 0.6), // E1: what Andy said
|
||
ev("actor:andy", DerivationConsolidation, 0.6), // S1: a semantic memory consolidated from it
|
||
ev("actor:andy", DerivationInference, 0.6), // I1: an inference drawn from S1
|
||
})
|
||
repeated := Compute(0.5, []Evidence{ev("actor:andy", DerivationAssertion, 0.6), ev("actor:andy", DerivationAssertion, 0.6), ev("actor:andy", DerivationAssertion, 0.6)})
|
||
if threeDerived.Confidence != one.Confidence {
|
||
t.Fatalf("derived copies inflated confidence: one=%.4f derived=%.4f", one.Confidence, threeDerived.Confidence)
|
||
}
|
||
if threeDerived.SourcesFor != 1 || threeIndependent.SourcesFor != 3 {
|
||
t.Fatalf("family counting wrong: derived=%d independent=%d", threeDerived.SourcesFor, threeIndependent.SourcesFor)
|
||
}
|
||
if !(threeIndependent.Confidence > repeated.Confidence && repeated.Confidence > one.Confidence) {
|
||
t.Fatalf("ordering wrong: independent=%.4f repeated=%.4f one=%.4f", threeIndependent.Confidence, repeated.Confidence, one.Confidence)
|
||
}
|
||
// Repetition is capped at 1.5× the strongest item however many times it recurs.
|
||
many := make([]Evidence, 20)
|
||
for i := range many {
|
||
many[i] = ev("actor:andy", DerivationAssertion, 0.6)
|
||
}
|
||
if got := Compute(0.5, many).For; got > 0.6*1.5+1e-9 {
|
||
t.Fatalf("repetition not capped: for=%.4f", got)
|
||
}
|
||
// Derived evidence stands in only when nothing primary exists for the family.
|
||
onlyDerived := Compute(0.5, []Evidence{ev("actor:andy", DerivationConsolidation, 0.3), ev("actor:andy", DerivationInference, 0.2)})
|
||
if onlyDerived.For != 0.3 {
|
||
t.Fatalf("only-derived family should count its strongest item once: %.4f", onlyDerived.For)
|
||
}
|
||
// Legacy rows: family falls back to source and they count as primary.
|
||
legacy := Compute(0.5, []Evidence{{Direction: "for", Kind: "event", RefID: "x", Source: "metrics", Weight: 0.6, Credibility: 1, Derivation: DerivationLegacy}, {Direction: "for", Kind: "event", RefID: "y", Source: "logs", Weight: 0.6, Credibility: 1}})
|
||
if legacy.SourcesFor != 2 || len(legacy.Families) != 2 {
|
||
t.Fatalf("legacy grouping: %+v", legacy.Families)
|
||
}
|
||
// Counter-evidence is grouped the same way.
|
||
against := Compute(0.5, []Evidence{{Direction: "against", Kind: "belief", RefID: "b", Source: "contradiction", Family: "belief:b", Derivation: DerivationInference, Weight: 0.5, Credibility: 0.8},
|
||
{Direction: "against", Kind: "belief", RefID: "b", Source: "contradiction", Family: "belief:b", Derivation: DerivationInference, Weight: 0.5, Credibility: 0.8}})
|
||
if against.Against != 0.4 {
|
||
t.Fatalf("against grouping: %.4f", against.Against)
|
||
}
|
||
// Families derive from event payloads: an actor beats a channel beats the component.
|
||
if f := FamilyForActor(" Andy "); f != "actor:andy" {
|
||
t.Fatal(f)
|
||
}
|
||
if f := FamilyForSource("Prometheus", "prod"); f != "source:prometheus:prod" {
|
||
t.Fatal(f)
|
||
}
|
||
}
|