feat(evidence): record provenance and stop derived evidence counting as corroboration

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.
This commit is contained in:
Andy Dixon
2026-07-31 21:14:52 +01:00
parent d86740af89
commit 0071658c93
18 changed files with 394 additions and 48 deletions
+2 -2
View File
@@ -110,7 +110,7 @@ func (s *Server) beliefsCreate(w http.ResponseWriter, r *http.Request) {
in.Weight = 0.7
}
res, err := s.deps.Beliefs.Create(r.Context(), s.deps.DB, s.Emitter(), beliefs.New{MindID: m.ID, Proposition: in.Proposition, Subject: in.Subject, Epistemic: in.Epistemic, Source: "operator",
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "operator:" + actor(r), Weight: in.Weight, Credibility: events.TrustOperator.Credibility(), Note: firstNonEmpty(in.Note, "asserted by operator "+actor(r))}},
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "operator:" + actor(r), Weight: in.Weight, Credibility: events.TrustOperator.Credibility(), Note: firstNonEmpty(in.Note, "asserted by operator "+actor(r)), Derivation: beliefs.DerivationOperator, Family: "operator:" + actor(r)}},
Alternatives: in.Alternatives})
if err != nil {
fail(w, r, beliefErr(err))
@@ -235,7 +235,7 @@ func (s *Server) beliefEvidence(w http.ResponseWriter, r *http.Request) {
if in.Kind == "" {
in.Kind = "operator"
}
res, err := s.deps.Beliefs.AddEvidence(r.Context(), s.deps.DB, s.Emitter(), m.ID, id, beliefs.EvidenceInput{Direction: in.Direction, Kind: in.Kind, RefID: in.RefID, Source: "operator:" + actor(r), Weight: in.Weight, Credibility: events.TrustOperator.Credibility(), Note: in.Note}, "operator evidence: "+in.Note, ids.Nil, ids.Nil)
res, err := s.deps.Beliefs.AddEvidence(r.Context(), s.deps.DB, s.Emitter(), m.ID, id, beliefs.EvidenceInput{Direction: in.Direction, Kind: in.Kind, RefID: in.RefID, Source: "operator:" + actor(r), Weight: in.Weight, Credibility: events.TrustOperator.Credibility(), Note: in.Note, Derivation: beliefs.DerivationOperator, Family: "operator:" + actor(r)}, "operator evidence: "+in.Note, ids.Nil, ids.Nil)
if err != nil {
fail(w, r, beliefErr(err))
return
+1 -1
View File
@@ -87,7 +87,7 @@ func (s *Server) mindImport(w http.ResponseWriter, r *http.Request) {
weight = 0.5
}
_, err := s.deps.Beliefs.Create(r.Context(), s.deps.DB, s.Emitter(), beliefs.New{MindID: m.ID, Proposition: sb.Proposition, Subject: sb.Subject, Epistemic: sb.Epistemic, Source: "seed",
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "seed:" + actor(r), Weight: weight, Credibility: events.TrustOperator.Credibility(), Note: firstNonEmpty(sb.Note, "seeded by "+actor(r))}}})
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "seed:" + actor(r), Weight: weight, Credibility: events.TrustOperator.Credibility(), Note: firstNonEmpty(sb.Note, "seeded by "+actor(r)), Derivation: beliefs.DerivationOperator, Family: "seed:" + actor(r)}}})
if err != nil {
if errors.Is(err, beliefs.ErrInvalid) || errors.Is(err, beliefs.ErrBlocked) {
problems = append(problems, "belief: "+err.Error())
+167 -22
View File
@@ -4,10 +4,13 @@
package beliefs
import (
"encoding/json"
"math"
"sort"
"strings"
"github.com/andydixon/noema/pkg/concepts"
"github.com/andydixon/noema/pkg/events"
)
// Status of a belief.
@@ -70,44 +73,186 @@ type Evidence struct {
Weight float64 `json:"weight"` // how strongly it bears on the proposition, (0,1]
Credibility float64 `json:"credibility"` // trust in the source, [0,1]
Retracted bool `json:"retracted"`
// Provenance. Family is the independent-source key (an actor, an
// integration, an operator); Derivation says how this row relates to its
// roots; RootIDs are the originating event or memory ids. Derived rows
// (consolidation, inference, reflection, dream, retrieval) never add
// weight to a family that already has a primary member: they carry no
// information their roots did not.
Derivation string `json:"derivation,omitempty"`
Family string `json:"family,omitempty"`
RootIDs []string `json:"root_ids,omitempty"`
ProvenanceStatus string `json:"provenance_status,omitempty"` // recorded | legacy_unknown
}
// Derivations. Primary derivations bring information from outside the
// belief system; derived ones re-express information already present.
const (
DerivationDirect = "direct" // an observation or event from the source itself
DerivationAssertion = "assertion" // somebody stated the proposition
DerivationOperator = "operator" // an operator asserted or endorsed it
DerivationExternal = "external" // external corroboration (integration, tool result)
DerivationPrediction = "prediction" // a prediction resolved against events
DerivationConsolidation = "consolidation" // generalised from episodes
DerivationInference = "inference" // reasoned from other items
DerivationReflection = "reflection" // produced by reflection or metacognition
DerivationDream = "dream" // proposed while dreaming
DerivationRetrieval = "retrieval" // a recalled memory offered as support
DerivationLegacy = "legacy" // written before provenance existed; treated as primary
)
// Derived reports whether a derivation re-expresses existing information.
func Derived(d string) bool {
switch d {
case DerivationConsolidation, DerivationInference, DerivationReflection, DerivationDream, DerivationRetrieval:
return true
}
return false
}
// FamilyOf returns the grouping key for a piece of evidence: its recorded
// family, else its source, else its reference.
func FamilyOf(e Evidence) string {
if e.Family != "" {
return e.Family
}
if e.Source != "" {
return e.Source
}
return e.Kind + ":" + e.RefID
}
// FamilyForActor is the family key for statements made by a person or agent.
func FamilyForActor(actor string) string {
actor = strings.ToLower(strings.TrimSpace(actor))
if actor == "" {
return ""
}
return "actor:" + actor
}
// FamilyForSource is the family key for an integration or channel that has
// no individual actor (metrics, webhooks, repositories).
func FamilyForSource(source, channel string) string {
source = strings.ToLower(strings.TrimSpace(source))
if source == "" {
return ""
}
if channel = strings.ToLower(strings.TrimSpace(channel)); channel != "" {
return "source:" + source + ":" + channel
}
return "source:" + source
}
// FamilyFromEvent derives the family of an observation from its payload:
// the actor who spoke, else the integration or channel it came from, else
// the event's source component.
func FamilyFromEvent(ev events.Event) string {
var p events.ObservationPayload
if len(ev.Payload) > 0 {
json.Unmarshal(ev.Payload, &p) //nolint:errcheck // absent payload fields simply fall through
}
if f := FamilyForActor(p.Actor); f != "" {
return f
}
if f := FamilyForSource(p.Source, p.Channel); f != "" {
return f
}
return FamilyForSource(ev.Source, "")
}
// FamilyShare explains one family's contribution to a confidence.
type FamilyShare struct {
Family string `json:"family"`
Direction string `json:"direction"`
Items int `json:"items"`
Primary int `json:"primary"`
Contribution float64 `json:"contribution"`
}
// Breakdown explains a confidence value.
type Breakdown struct {
Prior float64 `json:"prior"`
For float64 `json:"for"` // Σ weight×credibility, supporting
Against float64 `json:"against"` // Σ weight×credibility, opposing
SourcesFor int `json:"sources_for"` // distinct sources supporting
DiversityScale float64 `json:"diversity_scale"` // discount when support comes from one source
LogOdds float64 `json:"log_odds"`
Confidence float64 `json:"confidence"`
Formula string `json:"formula"`
Notes []string `json:"notes,omitempty"`
Prior float64 `json:"prior"`
Families []FamilyShare `json:"families,omitempty"` // per independent-source group
For float64 `json:"for"` // Σ weight×credibility, supporting
Against float64 `json:"against"` // Σ weight×credibility, opposing
SourcesFor int `json:"sources_for"` // distinct sources supporting
DiversityScale float64 `json:"diversity_scale"` // discount when support comes from one source
LogOdds float64 `json:"log_odds"`
Confidence float64 `json:"confidence"`
Formula string `json:"formula"`
Notes []string `json:"notes,omitempty"`
}
// Compute derives confidence from prior and evidence. Support from a single
// source is discounted (T4: poisoned memory) so repetition alone cannot make
// a belief strong.
// Compute derives confidence from prior and evidence.
//
// Evidence is grouped by family (independent source). Within a family the
// strongest primary item counts in full and each further primary item adds
// a quarter of its value, capped at one and a half times the strongest, so
// repetition from one source is worth little. Derived items (consolidation,
// inference, reflection, dream, retrieval) contribute only when their family
// has no primary item, and then only the strongest of them: a semantic
// memory consolidated from five episodes, and an inference drawn from it,
// re-express the episodes rather than corroborate them. Diversity counts
// families, not rows (T4: poisoned memory; review pass item 2).
func Compute(prior float64, ev []Evidence) Breakdown {
b := Breakdown{Prior: clampP(prior), Formula: "confidence = sigmoid(logit(prior) + slope × (for × diversity − against))"}
sources := map[string]struct{}{}
b := Breakdown{Prior: clampP(prior), Formula: "confidence = sigmoid(logit(prior) + slope × (for × diversity − against)); for/against sum per-family contributions (strongest primary + ¼ of the rest, capped at 1.5×; derived items only when no primary exists)"}
type group struct {
primary, derived []float64
items int
}
groups := map[string]map[string]*group{"for": {}, "against": {}}
var order []string
for _, e := range ev {
if e.Retracted || e.Weight <= 0 {
continue
}
v := clamp01(e.Weight) * clamp01(e.Credibility)
dir := "for"
if e.Direction == "against" {
b.Against += v
dir = "against"
}
fam := FamilyOf(e)
g := groups[dir][fam]
if g == nil {
g = &group{}
groups[dir][fam] = g
order = append(order, dir+"\x00"+fam)
}
g.items++
if Derived(e.Derivation) {
g.derived = append(g.derived, v)
} else {
g.primary = append(g.primary, v)
}
}
contribution := func(g *group) float64 {
if len(g.primary) > 0 {
sort.Sort(sort.Reverse(sort.Float64Slice(g.primary)))
c := g.primary[0]
for _, v := range g.primary[1:] {
c += 0.25 * v
}
return math.Min(c, 1.5*g.primary[0])
}
best := 0.0
for _, v := range g.derived {
best = math.Max(best, v)
}
return best
}
for _, key := range order {
dir, fam, _ := strings.Cut(key, "\x00")
g := groups[dir][fam]
c := contribution(g)
b.Families = append(b.Families, FamilyShare{Family: fam, Direction: dir, Items: g.items, Primary: len(g.primary), Contribution: round4(c)})
if dir == "against" {
b.Against += c
continue
}
b.For += v
key := e.Source
if key == "" {
key = e.Kind + ":" + e.RefID
}
sources[key] = struct{}{}
b.For += c
}
b.SourcesFor = len(sources)
b.SourcesFor = len(groups["for"])
// One source: 60% of face value; three or more independent sources: full.
b.DiversityScale = 1
if b.SourcesFor > 0 {
+58
View File
@@ -67,3 +67,61 @@ func TestContradicts(t *testing.T) {
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)
}
}
+29 -7
View File
@@ -136,6 +136,11 @@ type EvidenceInput struct {
Weight float64
Credibility float64
Note string
// Provenance (see Evidence). Family defaults to Source; Derivation
// defaults to direct. Producers that know their roots must say so.
Derivation string
Family string
RootIDs []string
}
// Result is a belief with its computed breakdown and the events written.
@@ -228,12 +233,29 @@ func normalise(e EvidenceInput) Evidence {
if e.Kind == "" {
e.Kind = "note"
}
return Evidence{Direction: e.Direction, Kind: e.Kind, RefID: e.RefID, Source: e.Source, Weight: e.Weight, Credibility: e.Credibility}
if e.Derivation == "" {
e.Derivation = DerivationDirect
}
if e.Family == "" {
e.Family = e.Source
}
return Evidence{Direction: e.Direction, Kind: e.Kind, RefID: e.RefID, Source: e.Source, Weight: e.Weight, Credibility: e.Credibility,
Derivation: e.Derivation, Family: e.Family, RootIDs: e.RootIDs, ProvenanceStatus: "recorded"}
}
func (s *Store) insertEvidence(ctx context.Context, q storage.Querier, b Belief, e Evidence, note string, eventID ids.ID) error {
_, err := q.Exec(ctx, `INSERT INTO belief_evidence (id, belief_id, mind_id, direction, kind, ref_id, source, weight, credibility, note, event_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
ids.New(), b.ID, b.MindID, e.Direction, e.Kind, e.RefID, e.Source, e.Weight, e.Credibility, note, nullID(eventID))
if e.Derivation == "" {
e.Derivation = DerivationDirect
}
if e.Family == "" {
e.Family = FamilyOf(e)
}
roots := e.RootIDs
if roots == nil {
roots = []string{}
}
_, err := q.Exec(ctx, `INSERT INTO belief_evidence (id, belief_id, mind_id, direction, kind, ref_id, source, weight, credibility, note, event_id, derivation, family, root_ids, provenance_status) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::uuid[],'recorded')`,
ids.New(), b.ID, b.MindID, e.Direction, e.Kind, e.RefID, e.Source, e.Weight, e.Credibility, note, nullID(eventID), e.Derivation, e.Family, roots)
return err
}
@@ -349,7 +371,7 @@ func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids
if q == nil {
q = s.db
}
rows, err := q.Query(ctx, `SELECT id, direction, kind, ref_id, source, weight, credibility, retracted, note, event_id, created_at FROM belief_evidence WHERE belief_id=$1 AND mind_id=$2 ORDER BY created_at`, id, mind)
rows, err := q.Query(ctx, `SELECT id, direction, kind, ref_id, source, weight, credibility, retracted, note, event_id, created_at, derivation, family, root_ids::text[], provenance_status FROM belief_evidence WHERE belief_id=$1 AND mind_id=$2 ORDER BY created_at`, id, mind)
if err != nil {
return nil, err
}
@@ -358,7 +380,7 @@ func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids
for rows.Next() {
var r EvidenceRow
var evID *string
if err := rows.Scan(&r.ID, &r.Direction, &r.Kind, &r.RefID, &r.Source, &r.Weight, &r.Credibility, &r.Retracted, &r.Note, &evID, &r.CreatedAt); err != nil {
if err := rows.Scan(&r.ID, &r.Direction, &r.Kind, &r.RefID, &r.Source, &r.Weight, &r.Credibility, &r.Retracted, &r.Note, &evID, &r.CreatedAt, &r.Derivation, &r.Family, &r.RootIDs, &r.ProvenanceStatus); err != nil {
return nil, err
}
if evID != nil {
@@ -519,10 +541,10 @@ func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emi
return nil, err
}
// Each belief becomes counter-evidence for the other, weighted by the other's confidence.
if err := s.insertEvidence(ctx, q, b, Evidence{Direction: "against", Kind: "belief", RefID: string(o.ID), Source: "contradiction", Weight: 0.5, Credibility: o.Confidence}, "contradicts: "+trunc(o.Proposition, 120), ev.ID); err != nil {
if err := s.insertEvidence(ctx, q, b, Evidence{Direction: "against", Kind: "belief", RefID: string(o.ID), Source: "contradiction", Weight: 0.5, Credibility: o.Confidence, Derivation: DerivationInference, Family: "belief:" + string(o.ID)}, "contradicts: "+trunc(o.Proposition, 120), ev.ID); err != nil {
return nil, err
}
if err := s.insertEvidence(ctx, q, o, Evidence{Direction: "against", Kind: "belief", RefID: string(b.ID), Source: "contradiction", Weight: 0.5, Credibility: b.Confidence}, "contradicts: "+trunc(b.Proposition, 120), ev.ID); err != nil {
if err := s.insertEvidence(ctx, q, o, Evidence{Direction: "against", Kind: "belief", RefID: string(b.ID), Source: "contradiction", Weight: 0.5, Credibility: b.Confidence, Derivation: DerivationInference, Family: "belief:" + string(b.ID)}, "contradicts: "+trunc(b.Proposition, 120), ev.ID); err != nil {
return nil, err
}
for _, x := range []Belief{b, o} {
+7 -1
View File
@@ -66,13 +66,19 @@ func (h Handlers) hypothesis(ctx context.Context, c *brain.Cycle, t workspace.Th
continue
}
cred := 0.6
family := "event:" + ref.ID
if e, err := c.Runtime.Events().Get(ctx, ids.ID(ref.ID)); err == nil {
var p events.ObservationPayload
if json.Unmarshal(e.Payload, &p) == nil && p.Trust.Valid() {
cred = p.Trust.Credibility()
}
if f := beliefs.FamilyFromEvent(e); f != "" {
family = f
}
}
ev = append(ev, beliefs.EvidenceInput{Direction: "for", Kind: "event", RefID: ref.ID, Source: "co-occurrence:" + ref.ID, Weight: 0.15, Credibility: cred, Note: "temporal co-occurrence (" + ref.Note + ")"})
// The two observations are the roots; the causal reading is an inference over them.
ev = append(ev, beliefs.EvidenceInput{Direction: "for", Kind: "event", RefID: ref.ID, Source: "co-occurrence:" + ref.ID, Weight: 0.15, Credibility: cred, Note: "temporal co-occurrence (" + ref.Note + ")",
Derivation: beliefs.DerivationInference, Family: family, RootIDs: []string{ref.ID}})
}
res, err := h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: c.State.Mind.ID, Proposition: prop, Epistemic: concepts.Hypothesis, Source: "brain.associate",
Evidence: ev, Alternatives: []string{"coincidence: the two events are unrelated", "a third factor caused both"}, OriginEvent: t.EventID, Causation: t.EventID, Correlation: c.State.CycleID,
+1 -1
View File
@@ -145,7 +145,7 @@ func (h Handlers) metacognition(ctx context.Context, c *brain.Cycle, t workspace
if bid, ok := t.Content["belief_id"].(string); ok && bid != "" && h.Beliefs != nil {
switch check {
case "insufficient_evidence", "single_source", "fluent_without_evidence":
_, err := h.Beliefs.AddEvidence(ctx, c.Tx, emitter(c), c.State.Mind.ID, ids.ID(bid), beliefs.EvidenceInput{Direction: "against", Kind: "metacognition", RefID: string(ev.ID), Source: "metacognition:" + check, Weight: 0.15 + 0.15*severity, Credibility: 0.9, Note: "self-check: " + check}, "metacognitive discount: "+check, ev.ID, c.State.CycleID)
_, err := h.Beliefs.AddEvidence(ctx, c.Tx, emitter(c), c.State.Mind.ID, ids.ID(bid), beliefs.EvidenceInput{Direction: "against", Kind: "metacognition", RefID: string(ev.ID), Source: "metacognition:" + check, Weight: 0.15 + 0.15*severity, Credibility: 0.9, Note: "self-check: " + check, Derivation: beliefs.DerivationReflection, Family: "metacognition"}, "metacognitive discount: "+check, ev.ID, c.State.CycleID)
if err != nil {
return err
}
+2 -1
View File
@@ -102,7 +102,8 @@ func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thou
w += 0.2
}
res, err := h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: cl.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID,
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "event", RefID: srcID, Source: "said-by:" + actor, Weight: w, Credibility: trust.Credibility(), Note: actor + " " + cl.Certainty + " it"}}})
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "event", RefID: srcID, Source: "said-by:" + actor, Weight: w, Credibility: trust.Credibility(), Note: actor + " " + cl.Certainty + " it",
Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}}}})
if err != nil {
c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}})
continue
+13 -1
View File
@@ -307,7 +307,19 @@ func (s *Service) consolidate(ctx context.Context, m minds.Mind, c Cluster, llmB
var refs []events.EvidenceRef
var epIDs []ids.ID
for _, e := range c.Episodes {
ev = append(ev, beliefs.EvidenceInput{Direction: "for", Kind: "memory", RefID: string(e.ID), Source: e.Source + "/" + string(e.Trust), Weight: 0.3, Credibility: e.Credibility, Note: "episode: " + truncate(e.Content, 80)})
// Ancestry: the episode's origin event names the actor or channel it
// came from; five episodes from one person are one family.
family, roots := "memory:"+e.Source, []string{string(e.ID)}
if !e.OriginEvent.IsZero() {
roots = []string{string(e.OriginEvent)}
if oe, err := s.Events.Get(ctx, e.OriginEvent); err == nil {
if f := beliefs.FamilyFromEvent(oe); f != "" {
family = f
}
}
}
ev = append(ev, beliefs.EvidenceInput{Direction: "for", Kind: "memory", RefID: string(e.ID), Source: e.Source + "/" + string(e.Trust), Weight: 0.3, Credibility: e.Credibility, Note: "episode: " + truncate(e.Content, 80),
Derivation: beliefs.DerivationConsolidation, Family: family, RootIDs: roots})
refs = append(refs, events.EvidenceRef{Kind: "memory", ID: string(e.ID), Note: "episode"})
epIDs = append(epIDs, e.ID)
}
+23 -2
View File
@@ -386,8 +386,10 @@ func (s *Service) hypothesise(ctx context.Context, m minds.Mind, dream ids.ID, p
return out[0], nil
}
ev := []beliefs.EvidenceInput{
{Direction: "for", Kind: "memory", RefID: string(p.A.ID), Source: "dreaming/" + p.A.Source, Weight: 0.1, Credibility: p.A.Credibility, Note: "dream replay: " + excerpt(p.A.Content, 80)},
{Direction: "for", Kind: "memory", RefID: string(p.B.ID), Source: "dreaming/" + p.B.Source, Weight: 0.1, Credibility: p.B.Credibility, Note: "dream replay: " + excerpt(p.B.Content, 80)},
{Direction: "for", Kind: "memory", RefID: string(p.A.ID), Source: "dreaming/" + p.A.Source, Weight: 0.1, Credibility: p.A.Credibility, Note: "dream replay: " + excerpt(p.A.Content, 80),
Derivation: beliefs.DerivationDream, Family: s.familyOf(ctx, p.A), RootIDs: rootsOf(p.A)},
{Direction: "for", Kind: "memory", RefID: string(p.B.ID), Source: "dreaming/" + p.B.Source, Weight: 0.1, Credibility: p.B.Credibility, Note: "dream replay: " + excerpt(p.B.Content, 80),
Derivation: beliefs.DerivationDream, Family: s.familyOf(ctx, p.B), RootIDs: rootsOf(p.B)},
}
detail := map[string]any{"dream_id": dream, "wording": w.Wording, "rationale": w.Rationale, "shared": p.Shared}
if !w.CallID.IsZero() {
@@ -517,3 +519,22 @@ func excerpt(s string, n int) string {
}
return s
}
// familyOf traces a memory to the actor or channel behind its origin event.
func (s *Service) familyOf(ctx context.Context, m memory.Memory) string {
if !m.OriginEvent.IsZero() && s.Events != nil {
if ev, err := s.Events.Get(ctx, m.OriginEvent); err == nil {
if f := beliefs.FamilyFromEvent(ev); f != "" {
return f
}
}
}
return "memory:" + m.Source
}
func rootsOf(m memory.Memory) []string {
if !m.OriginEvent.IsZero() {
return []string{string(m.OriginEvent)}
}
return []string{string(m.ID)}
}
+13 -1
View File
@@ -128,7 +128,8 @@ func (r *Resolver) ResolveIn(ctx context.Context, q storage.Querier, p Predictio
evs = append(evs, out[0])
return out[0], nil
}
if _, err := r.Beliefs.AddEvidence(ctx, q, emit, p.MindID, p.BeliefID, beliefs.EvidenceInput{Direction: dir, Kind: "prediction", RefID: string(p.ID), Source: "prediction:" + string(p.ID), Weight: weight, Credibility: 0.9, Note: "prediction " + string(o.Status) + ": " + p.Statement}, "prediction "+string(o.Status), evs[0].ID, ids.Nil); err != nil {
if _, err := r.Beliefs.AddEvidence(ctx, q, emit, p.MindID, p.BeliefID, beliefs.EvidenceInput{Direction: dir, Kind: "prediction", RefID: string(p.ID), Source: "prediction:" + string(p.ID), Weight: weight, Credibility: 0.9, Note: "prediction " + string(o.Status) + ": " + p.Statement,
Derivation: beliefs.DerivationPrediction, Family: "prediction:" + string(p.ID), RootIDs: refIDs(o.Evidence)}, "prediction "+string(o.Status), evs[0].ID, ids.Nil); err != nil {
r.Log.Warn("belief feedback failed", "belief", p.BeliefID, "err", err)
}
}
@@ -141,3 +142,14 @@ func minTime(a, b time.Time) time.Time {
}
return b
}
// refIDs lists the event ids an outcome was judged against.
func refIDs(refs []events.EvidenceRef) []string {
var out []string
for _, r := range refs {
if r.Kind == "event" && r.ID != "" {
out = append(out, r.ID)
}
}
return out
}
+34
View File
@@ -3,12 +3,17 @@ package replay
import (
"context"
"log/slog"
"sort"
"strings"
"testing"
"github.com/andydixon/noema/internal/beliefs"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/minds"
"github.com/andydixon/noema/internal/replay/replaytest"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
func TestReplayIsExactWithoutModel(t *testing.T) {
@@ -51,6 +56,11 @@ func TestReplayIsExactWithoutModel(t *testing.T) {
if err != nil || sandbox.Status != minds.StatusDisabled || sandbox.ForkedFromSnapshot == nil {
t.Fatalf("sandbox mind wrong: %+v %v", sandbox, err)
}
// Provenance is reconstructed: beliefs in the sandbox cite the same
// families in the same derivations as the source's.
if a, b := provenanceDigest(t, h, src.ID), provenanceDigest(t, h, sandbox.ID); a != b {
t.Fatalf("provenance differs after replay:\nsource: %s\nsandbox: %s", a, b)
}
// The source's history is untouched by the replay.
after, _ := h.Events.Query(ctx, evstore.Filter{MindID: src.ID, Limit: 1})
if after[0].Seq != last[0].Seq {
@@ -62,3 +72,27 @@ func TestReplayIsExactWithoutModel(t *testing.T) {
}
_ = events.Observation
}
// provenanceDigest summarises a mind's beliefs as proposition → sorted
// (derivation, family) pairs, ignoring ids that differ between minds.
func provenanceDigest(t *testing.T, h *replaytest.Harness, mind ids.ID) string {
t.Helper()
ctx := context.Background()
bs := h.Brain.Services.Beliefs
list, err := bs.List(ctx, mind, beliefs.Filter{Limit: 200})
if err != nil {
t.Fatal(err)
}
var lines []string
for _, b := range list {
rows, _ := bs.EvidenceFor(ctx, nil, mind, b.ID)
var ev []string
for _, r := range rows {
ev = append(ev, r.Derivation+"/"+r.Family+"/"+r.ProvenanceStatus)
}
sort.Strings(ev)
lines = append(lines, strings.ToLower(b.Proposition)+" ← "+strings.Join(ev, ","))
}
sort.Strings(lines)
return strings.Join(lines, "\n")
}