feat: add beliefs with evidence-derived confidence, contradictions and corrections
Phase 6. Beliefs are propositions with a prior set by epistemic kind and a confidence computed from typed evidence: sigmoid(logit(prior) + slope × (support × diversity − opposition)), where support from a single source is discounted so repetition cannot manufacture certainty, and bounds keep the mind away from absolute certainty. Every evaluation stores its breakdown and appends a version row protected by the append-only trigger; the belief page shows the arithmetic, evidence for and against, alternatives, contradictions and history. Contradiction detection is deterministic first (negation plus token overlap): a hit emits contradiction_detected, links both beliefs and makes each counter-evidence for the other so both trend to contested. Operators can assert beliefs (their statement is the evidence), add evidence, mark a belief wrong (pinned rejected, history kept) and forbid an inference; do-not-infer corrections block matching hypotheses. Retracting a memory retracts the evidence that cited it and re-evaluates the beliefs concerned. The associate process proposes low-confidence causal hypotheses from co-occurring notable observations, and the hypothesis handler turns admitted ones into beliefs with automatic alternatives, closing demo scenarios 1 and 2 without any model call.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package beliefs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/minds"
|
||||
"github.com/andydixon/noema/internal/storage"
|
||||
"github.com/andydixon/noema/migrations"
|
||||
"github.com/andydixon/noema/pkg/concepts"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
func TestBeliefLifecycleProvenanceAndContradiction(t *testing.T) {
|
||||
pool := storage.TestPool(t, migrations.FS)
|
||||
ctx := context.Background()
|
||||
m, _ := minds.NewStore(pool).Create(ctx, minds.CreateParams{Slug: "b"})
|
||||
es := evstore.NewStore(pool, evstore.NewBus(8), nil)
|
||||
emit := func(ctx context.Context, q storage.Querier, n events.New) (events.Event, error) {
|
||||
out, err := es.AppendIn(ctx, q, n)
|
||||
if err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
return out[0], nil
|
||||
}
|
||||
s := NewStore(pool)
|
||||
obs1s, _ := es.Append(ctx, events.New{MindID: m.ID, Type: events.Observation, Source: "smart", Summary: "SMART CRC errors on server-01", Salience: 0.8, Confidence: 1})
|
||||
obs2s, _ := es.Append(ctx, events.New{MindID: m.ID, Type: events.Observation, Source: "metrics", Summary: "I/O latency high on server-01", Salience: 0.8, Confidence: 1})
|
||||
obs1, obs2 := obs1s[0], obs2s[0]
|
||||
|
||||
res, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "server-01 has an intermittent storage problem", Subject: "server-01", Epistemic: concepts.Hypothesis, Source: "reasoning",
|
||||
Evidence: []EvidenceInput{{Direction: "for", Kind: "event", RefID: string(obs1.ID), Source: "smart", Weight: 0.6, Credibility: 0.9, Note: "CRC errors"}},
|
||||
Alternatives: []string{"SATA cable issue", "controller issue"}, OriginEvent: obs1.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := res.Belief
|
||||
if b.Status != StatusHypothesis && b.Status != StatusTentative || b.Confidence <= PriorFor(concepts.Hypothesis) || res.Events[0].Type != events.HypothesisCreated {
|
||||
t.Fatalf("create wrong: %+v %+v", b, res.Events)
|
||||
}
|
||||
if len(res.Events[0].Evidence) != 2 {
|
||||
t.Fatalf("provenance refs missing: %+v", res.Events[0].Evidence)
|
||||
}
|
||||
// Support from a second, independent source raises confidence more than repetition.
|
||||
r2, err := s.AddEvidence(ctx, pool, emit, m.ID, b.ID, EvidenceInput{Direction: "for", Kind: "event", RefID: string(obs2.ID), Source: "metrics", Weight: 0.6, Credibility: 0.9, Note: "latency"}, "observed latency", obs2.ID, ids.Nil)
|
||||
if err != nil || r2.Belief.Confidence <= b.Confidence || r2.Belief.Version != 2 || r2.Events[0].Type != events.HypothesisSupported {
|
||||
t.Fatalf("support wrong: %v %+v", err, r2)
|
||||
}
|
||||
// Counter-evidence lowers it and may contest.
|
||||
r3, err := s.AddEvidence(ctx, pool, emit, m.ID, b.ID, EvidenceInput{Direction: "against", Kind: "note", Source: "smart", Weight: 0.7, Credibility: 0.9, Note: "extended SMART test passed"}, "test passed", ids.Nil, ids.Nil)
|
||||
if err != nil || r3.Belief.Confidence >= r2.Belief.Confidence || r3.Events[0].Type != events.HypothesisWeakened {
|
||||
t.Fatalf("weaken wrong: %v %+v", err, r3)
|
||||
}
|
||||
vs, _ := s.Versions(ctx, m.ID, b.ID)
|
||||
if len(vs) != 3 || vs[0].Version != 3 || vs[0].Breakdown.Against == 0 {
|
||||
t.Fatalf("versions wrong: %+v", vs)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `DELETE FROM belief_versions WHERE belief_id=$1`, b.ID); err == nil {
|
||||
t.Fatal("belief history must be append-only")
|
||||
}
|
||||
alts, _ := s.Alternatives(ctx, m.ID, b.ID)
|
||||
if len(alts) != 2 {
|
||||
t.Fatalf("alternatives missing: %+v", alts)
|
||||
}
|
||||
|
||||
// A negated proposition triggers a contradiction and links both beliefs.
|
||||
rc, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "server-01 does not have an intermittent storage problem", Subject: "server-01", Epistemic: concepts.Claim, Source: "user",
|
||||
Evidence: []EvidenceInput{{Direction: "for", Kind: "note", Source: "user", Weight: 0.5, Credibility: 0.85}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sawContra bool
|
||||
for _, ev := range rc.Events {
|
||||
if ev.Type == events.ContradictionDetected {
|
||||
sawContra = true
|
||||
}
|
||||
}
|
||||
if !sawContra {
|
||||
t.Fatalf("no contradiction event: %+v", rc.Events)
|
||||
}
|
||||
links, _ := s.Links(ctx, m.ID, b.ID)
|
||||
if len(links) != 1 || links[0].Relation != "contradicts" || links[0].Other.ID != rc.Belief.ID {
|
||||
t.Fatalf("links wrong: %+v", links)
|
||||
}
|
||||
after, _ := s.Get(ctx, pool, m.ID, b.ID)
|
||||
if after.Confidence >= r3.Belief.Confidence {
|
||||
t.Fatalf("contradiction should lower confidence: %v -> %v", r3.Belief.Confidence, after.Confidence)
|
||||
}
|
||||
|
||||
// Operator correction: rejected, pinned, history kept, do-not-infer blocks recreation.
|
||||
rr, err := s.Retract(ctx, emit, m.ID, b.ID, "it was the SATA cable", "andy", true)
|
||||
if err != nil || rr.Belief.Status != StatusRejected || rr.Events[0].Type != events.BeliefRetracted {
|
||||
t.Fatalf("retract wrong: %v %+v", err, rr)
|
||||
}
|
||||
got, _ := s.Get(ctx, pool, m.ID, b.ID)
|
||||
if got.Status != StatusRejected || got.PinnedStatus == nil {
|
||||
t.Fatalf("not pinned rejected: %+v", got)
|
||||
}
|
||||
vs, _ = s.Versions(ctx, m.ID, b.ID)
|
||||
if len(vs) < 4 {
|
||||
t.Fatalf("history lost on retraction: %d", len(vs))
|
||||
}
|
||||
if _, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "server-01 has an intermittent storage problem", Epistemic: concepts.Hypothesis}); !errors.Is(err, ErrBlocked) {
|
||||
t.Fatalf("do-not-infer not enforced: %v", err)
|
||||
}
|
||||
cs, _ := s.Corrections(ctx, m.ID, "")
|
||||
if len(cs) != 2 {
|
||||
t.Fatalf("corrections missing: %+v", cs)
|
||||
}
|
||||
// Isolation: another mind cannot see these.
|
||||
other, _ := minds.NewStore(pool).Create(ctx, minds.CreateParams{Slug: "other"})
|
||||
if _, err := s.Get(ctx, pool, other.ID, b.ID); err == nil {
|
||||
t.Fatal("cross-mind belief read")
|
||||
}
|
||||
list, _ := s.List(ctx, other.ID, Filter{})
|
||||
if len(list) != 0 {
|
||||
t.Fatal("cross-mind belief list")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user