feat(beliefs): represent unknown explicitly; absence of evidence is not falsity

A belief with no live evidence on either side now derives the status
'unknown' instead of sitting tentatively at its prior. Withdrawing every
piece of evidence leaves the proposition unknown, never rejected, and
introspection answers 'I do not know whether X' rather than quoting the
prior. Hypotheses keep their own status because they are proposals by
construction. Migration 0019 widens the status constraint; the docs define
every epistemic number and how absence of evidence is handled.
This commit is contained in:
Andy Dixon
2026-08-05 23:04:15 +01:00
parent 9b2521bffd
commit 08a265f0fa
8 changed files with 91 additions and 4 deletions
+10 -2
View File
@@ -19,6 +19,7 @@ type Status string
// Statuses in rough order of commitment.
const (
StatusHypothesis Status = "hypothesis"
StatusUnknown Status = "unknown" // recorded, but no live evidence for or against: not false, not known
StatusTentative Status = "tentative"
StatusLikely Status = "likely"
StatusAccepted Status = "accepted"
@@ -30,7 +31,7 @@ const (
// Valid reports whether s is known.
func (s Status) Valid() bool {
switch s {
case StatusHypothesis, StatusTentative, StatusLikely, StatusAccepted, StatusContested, StatusRejected, StatusSuperseded:
case StatusHypothesis, StatusTentative, StatusLikely, StatusAccepted, StatusContested, StatusRejected, StatusSuperseded, StatusUnknown:
return true
}
return false
@@ -269,11 +270,18 @@ func Compute(prior float64, ev []Evidence) Breakdown {
}
// Derive maps a breakdown to a status. Contested requires real evidence on
// both sides; rejected requires the evidence to have pushed confidence low.
// both sides; rejected requires the evidence to have pushed confidence low;
// unknown means nothing live bears on the proposition either way, which is
// deliberately distinct from tentative belief at the prior (absence of
// evidence is not evidence of falsity). Hypotheses keep their own status
// because they are proposals awaiting evidence by construction.
func Derive(b Breakdown, epistemic concepts.Epistemic) Status {
if b.For > 0 && b.Against > 0 && b.Against >= 0.5*b.For*b.DiversityScale && b.Confidence >= 0.2 && b.Confidence <= 0.8 {
return StatusContested
}
if b.For == 0 && b.Against == 0 && epistemic != concepts.Hypothesis {
return StatusUnknown
}
switch {
case b.Confidence < 0.15:
return StatusRejected
+41
View File
@@ -120,3 +120,44 @@ func TestBeliefLifecycleProvenanceAndContradiction(t *testing.T) {
t.Fatal("cross-mind belief list")
}
}
// TestUnknownIsNotFalse: withdrawing every piece of evidence leaves a belief
// unknown, not tentatively held at its prior and not rejected.
func TestUnknownIsNotFalse(t *testing.T) {
if Derive(Breakdown{Prior: 0.5, Confidence: 0.5}, concepts.Claim) != StatusUnknown {
t.Fatal("no evidence should derive unknown")
}
if Derive(Breakdown{Prior: 0.25, Confidence: 0.25}, concepts.Hypothesis) != StatusHypothesis {
t.Fatal("hypotheses keep their own status")
}
pool := storage.TestPool(t, migrations.FS)
ctx := context.Background()
m, _ := minds.NewStore(pool).Create(ctx, minds.CreateParams{Slug: "unk"})
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)
res, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the cache is warm", Epistemic: concepts.Claim, Source: "test",
Evidence: []EvidenceInput{{Direction: "for", Kind: "memory", RefID: "mem-1", Source: "user", Weight: 0.6, Credibility: 0.9}}})
if err != nil || res.Belief.Status == StatusUnknown {
t.Fatalf("create: %+v %v", res.Belief, err)
}
if _, err := s.RetractEvidence(ctx, pool, m.ID, "memory", "mem-1"); err != nil {
t.Fatal(err)
}
if _, err := s.Reevaluate(ctx, pool, emit, m.ID, res.Belief.ID, "memory retracted"); err != nil {
t.Fatal(err)
}
b, _ := s.Get(ctx, pool, m.ID, res.Belief.ID)
if b.Status != StatusUnknown {
t.Fatalf("after retraction status=%s (%.2f); want unknown", b.Status, b.Confidence)
}
if b.Status == StatusRejected {
t.Fatal("absence of evidence must not read as falsity")
}
}