diff --git a/docs/beliefs.md b/docs/beliefs.md index 54499bb..139c8f5 100644 --- a/docs/beliefs.md +++ b/docs/beliefs.md @@ -13,6 +13,22 @@ A **belief** is a proposition the mind holds with an explicit, evidence-derived | `belief_versions` | append-only history with the confidence breakdown and the event that caused each change | | `corrections` | operator statements: belief wrong, memory inaccurate, do not infer, entity same/different | +## What the numbers mean + +Every numeric epistemic value in Noema has one defined meaning. None of them is a calibrated probability unless the page says so. + +| Value | Range | Meaning | +|---|---|---| +| **epistemic score** (`confidence` on a belief) | [0.02, 0.98] | the deterministic output of the arithmetic below over the recorded evidence: a ranking of how well supported a proposition is *given what this mind has seen and how it weighs it*. It is comparable between beliefs of one mind; it is **not** "an 83 % chance of being true". | +| **calibrated estimate** | [0, 1] or absent | the observed hit rate of this mind's *resolved predictions* whose stated confidence fell in the same range as the score, shown only when at least 20 predictions have been decided in that range. Predictions are the one place a score meets reality, so they are the only empirical basis offered. | +| **prior** | (0, 1) | the starting score for the epistemic kind before evidence (hypothesis 0.25, claim 0.5, fact 0.8, …). | +| **weight** (evidence) | (0, 1] | how strongly a piece of evidence bears on the proposition, set by the producer: an emphatic first-hand assertion weighs more than a hedged second-hand one; a prediction miss weighs 0.5. | +| **credibility** (evidence) | [0, 1] | how much the source is trusted for this row: the trust level of the channel (operator 0.95, user 0.85, system 0.9, tool 0.8, source 0.7, low 0.4), adjusted by the source's recorded reliability in the belief's domain where enough history exists. | +| **contribution** (family) | ≥ 0 | what one independent source adds after repetition and derivation discounts. | +| **diversity** | [0.6, 1] | the multiplier on support that reflects how many independent families agree. | +| **interpretation confidence** (assertion) | [0, 1] | how sure the deterministic reading of an utterance is (who is attributed, what stance, whether it was sarcasm or a hypothetical). It is about understanding the speaker, not about the world. | +| **primary uncertainty** | text | the deterministic reason the score should be doubted most: a single family, second-hand only, conflicting evidence, little evidence, or an unclear reading. | + ## Confidence arithmetic Confidence is never typed in by a person or produced by a model. It is: @@ -43,6 +59,13 @@ Statements people make reach beliefs only through assertions (`docs/conversation ## Status +Statuses: `unknown` (recorded, but no live evidence for or against), `hypothesis` (a proposal awaiting evidence), `tentative`, `likely`, `accepted`, `contested` (real evidence on both sides), `rejected` (evidence or an operator pushed it low) and `superseded` (replaced by a corrected proposition). + +### Absence of evidence + +Lack of evidence never collapses towards "false". A belief whose evidence has all been retracted, or that was recorded without any, becomes `unknown` and its score is not quoted; introspection answers "I do not know whether X". A belief is `rejected` only when evidence against it, or an operator, drove its score below 0.15. Hypotheses are the one exception: they carry the `hypothesis` status until evidence arrives because they are proposals by construction. + + Derived from the breakdown unless an operator pinned it: - `contested` when both sides carry real weight (against ≥ half of the discounted support) and confidence is mid-range; diff --git a/internal/beliefs/confidence.go b/internal/beliefs/confidence.go index 5d7ba15..6bfe7a4 100644 --- a/internal/beliefs/confidence.go +++ b/internal/beliefs/confidence.go @@ -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 diff --git a/internal/beliefs/store_test.go b/internal/beliefs/store_test.go index 6f2f811..a096d98 100644 --- a/internal/beliefs/store_test.go +++ b/internal/beliefs/store_test.go @@ -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") + } +} diff --git a/internal/cognition/associate.go b/internal/cognition/associate.go index 2bcd3f5..f084f6e 100644 --- a/internal/cognition/associate.go +++ b/internal/cognition/associate.go @@ -1,8 +1,8 @@ package cognition import ( - "encoding/json" "context" + "encoding/json" "sort" "strings" "time" diff --git a/internal/cognition/respond.go b/internal/cognition/respond.go index 2575586..def380b 100644 --- a/internal/cognition/respond.go +++ b/internal/cognition/respond.go @@ -289,6 +289,11 @@ func (h Handlers) addKnowledge(ctx context.Context, c *brain.Cycle, in *Intent, if b.Status == beliefs.StatusRejected { continue } + if b.Status == beliefs.StatusUnknown { + // Recorded but unsupported either way: say so rather than quote the prior. + in.Points = append(in.Points, "I do not know whether "+b.Proposition+": it was recorded, but nothing live supports or opposes it.") + continue + } in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)}) in.Points = append(in.Points, fmt.Sprintf("%s (%s, %.2f)", b.Proposition, b.Status, b.Confidence)) if b.Confidence > maxConf { diff --git a/migrations/0019_belief_status_unknown.down.sql b/migrations/0019_belief_status_unknown.down.sql new file mode 100644 index 0000000..2adf1bb --- /dev/null +++ b/migrations/0019_belief_status_unknown.down.sql @@ -0,0 +1,4 @@ +UPDATE beliefs SET status='tentative' WHERE status='unknown'; +ALTER TABLE beliefs DROP CONSTRAINT IF EXISTS beliefs_status_check; +ALTER TABLE beliefs ADD CONSTRAINT beliefs_status_check + CHECK (status IN ('hypothesis','tentative','likely','accepted','contested','rejected','superseded')); diff --git a/migrations/0019_belief_status_unknown.up.sql b/migrations/0019_belief_status_unknown.up.sql new file mode 100644 index 0000000..3633539 --- /dev/null +++ b/migrations/0019_belief_status_unknown.up.sql @@ -0,0 +1,6 @@ +-- Explicit unknown (review pass §7.2 item 13): a belief with no live evidence +-- on either side is 'unknown', not tentatively held at its prior. Absence +-- of evidence is not evidence of falsity. +ALTER TABLE beliefs DROP CONSTRAINT IF EXISTS beliefs_status_check; +ALTER TABLE beliefs ADD CONSTRAINT beliefs_status_check + CHECK (status IN ('hypothesis','tentative','likely','accepted','contested','rejected','superseded','unknown')); diff --git a/web/static/app.css b/web/static/app.css index 5ac7bfe..7d45a30 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -171,7 +171,7 @@ tr.dayrow td { background: var(--surface-2); font-weight: 600; font-size: .78rem /* Beliefs */ .badge.st-hypothesis { background: var(--surface-2); color: var(--muted); } .badge.st-tentative { background: #fef3c7; color: #b45309; } .badge.st-likely { background: #dbeafe; color: #1d4ed8; } -.badge.st-accepted { background: var(--ok-soft); color: var(--ok); } .badge.st-contested { background: #fde68a; color: #92400e; } .badge.st-rejected { background: var(--danger-soft); color: var(--danger); } .badge.st-superseded { background: var(--surface-2); color: var(--muted); text-decoration: line-through; } +.badge.st-accepted { background: var(--ok-soft); color: var(--ok); } .badge.st-contested { background: #fde68a; color: #92400e; } .badge.st-rejected { background: var(--danger-soft); color: var(--danger); } .badge.st-superseded { background: var(--surface-2); color: var(--muted); text-decoration: line-through; } .badge.st-unknown { background: var(--surface-2); color: var(--muted); border: 1px dashed var(--border); } :root[data-theme="dark"] .badge.st-tentative { background: #3a2a0a; color: #fcd34d; } :root[data-theme="dark"] .badge.st-likely { background: #172554; color: #93c5fd; } :root[data-theme="dark"] .badge.st-contested { background: #3d2e05; color: #fbbf24; } .conf.big .conf-bar.wide { width: 100%; height: 10px; margin: .3rem 0 .8rem; } ul.ev li { padding: .4rem 0; border-bottom: 1px solid var(--border); } ul.ev li:last-child { border-bottom: 0; } ul.ev li.retracted { opacity: .55; }