Every breakdown now carries its meaning: an uncalibrated epistemic score derived from recorded evidence, not a probability of truth. Belief detail adds a calibrated estimate read from the mind's resolved predictions when at least twenty were decided in the score's range, and otherwise says why none is shown; and names the primary uncertainty deterministically (no live evidence, conflicting evidence, second-hand only, unclear reading, a single source family, little evidence). The belief list heading becomes Score.
171 lines
9.4 KiB
Go
171 lines
9.4 KiB
Go
package beliefs
|
||
|
||
import (
|
||
"strings"
|
||
"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)
|
||
}
|
||
}
|
||
|
||
// TestCalibrationIsGatedAndDeterministic: a calibrated estimate appears only
|
||
// with enough decided predictions in the score's range, and the primary
|
||
// uncertainty names the right doubt.
|
||
func TestCalibrationIsGatedAndDeterministic(t *testing.T) {
|
||
buckets := []CalibrationBucket{{0, 0.5, 3, 0.33}, {0.5, 0.8, 25, 0.64}, {0.8, 1.01, 40, 0.9}}
|
||
if c := CalibrateWith(0.3, buckets); c.Available || c.SampleSize != 3 || c.Reason == "" {
|
||
t.Fatalf("small sample must not calibrate: %+v", c)
|
||
}
|
||
if c := CalibrateWith(0.59, buckets); !c.Available || c.Estimate != 0.64 || c.SampleSize != 25 {
|
||
t.Fatalf("calibrated: %+v", c)
|
||
}
|
||
if c := CalibrateWith(0.59, nil); c.Available || c.Reason == "" {
|
||
t.Fatalf("no buckets: %+v", c)
|
||
}
|
||
if b := Compute(0.5, nil); b.Meaning != ScoreMeaning {
|
||
t.Fatal("breakdown must carry its meaning")
|
||
}
|
||
rows := func(rs ...EvidenceRow) ([]Evidence, []EvidenceRow) {
|
||
var ev []Evidence
|
||
for _, r := range rs {
|
||
ev = append(ev, r.Evidence)
|
||
}
|
||
return ev, rs
|
||
}
|
||
one := EvidenceRow{Evidence: Evidence{Direction: "for", Kind: "assertion", Source: "asserted-by:andy", Family: "actor:andy", Derivation: DerivationAssertion, Weight: 0.5, Credibility: 0.9}, Note: "andy asserted it (read with 85% interpretation confidence)"}
|
||
ev, rs := rows(one)
|
||
if u := PrimaryUncertainty(Compute(0.5, ev), rs); u != "evidence independence: a single source family" {
|
||
t.Fatal(u)
|
||
}
|
||
ev, rs = rows(EvidenceRow{Evidence: Evidence{Direction: "for", Kind: "assertion", Source: "asserted-by:alice", Family: "actor:alice", Derivation: DerivationAssertion, Weight: 0.2, Credibility: 0.6}, Note: "andy reported that alice holds this (asserted, read with 75% interpretation confidence)"})
|
||
if u := PrimaryUncertainty(Compute(0.5, ev), rs); !strings.HasPrefix(u, "second-hand") {
|
||
t.Fatal(u)
|
||
}
|
||
ev, rs = rows(one, EvidenceRow{Evidence: Evidence{Direction: "against", Kind: "operator", Source: "operator:x", Family: "operator:x", Weight: 0.5, Credibility: 0.9}})
|
||
if u := PrimaryUncertainty(Compute(0.5, ev), rs); u != "conflicting evidence" {
|
||
t.Fatal(u)
|
||
}
|
||
if u := PrimaryUncertainty(Compute(0.5, nil), nil); u != "no live evidence" {
|
||
t.Fatal(u)
|
||
}
|
||
}
|