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:
@@ -9,6 +9,9 @@ All notable changes to Noema are recorded here. Format follows Keep a Changelog;
|
|||||||
- NUL bytes in any request path, query or body are refused with a 400 at the edge instead of surfacing as a database error.
|
- NUL bytes in any request path, query or body are refused with a 400 at the edge instead of surfacing as a database error.
|
||||||
- Security suite: SQL-injection effect test, static allow-list scan for assembled SQL, deceived-mind authority test, garbage model output test, self-preservation vocabulary test; threat model rewritten so every verification cites a test that asserts the claim.
|
- Security suite: SQL-injection effect test, static allow-list scan for assembled SQL, deceived-mind authority test, garbage model output test, self-preservation vocabulary test; threat model rewritten so every verification cites a test that asserts the claim.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Evidence provenance: every evidence row records its derivation, independent-source family and root ids; confidence groups evidence by family so derived material (consolidation, inference, reflection, dreams, retrieval) never counts as corroboration of its own roots and repetition within one source is capped. Existing rows are marked `legacy_unknown` rather than given invented ancestry. Migration 0017.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- ADR-0002 no longer describes the event sequence as gap-free; cursors tolerate gaps by construction and a regression test proves it.
|
- ADR-0002 no longer describes the event sequence as gap-free; cursors tolerate gaps by construction and a regression test proves it.
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -18,17 +18,24 @@ A **belief** is a proposition the mind holds with an explicit, evidence-derived
|
|||||||
Confidence is never typed in by a person or produced by a model. It is:
|
Confidence is never typed in by a person or produced by a model. It is:
|
||||||
|
|
||||||
```
|
```
|
||||||
for = Σ weight × credibility over supporting evidence
|
value(e) = weight × credibility for each live evidence row e
|
||||||
against = Σ weight × credibility over opposing evidence
|
family(e) = the independent source e ultimately rests on (an actor, an integration, an operator, a prediction)
|
||||||
diversity = 0.6 + 0.4 × min(1, (distinct supporting sources − 1) / 2)
|
contribution(family) = strongest primary item + ¼ × each further primary item, capped at 1.5 × the strongest
|
||||||
confidence = sigmoid( logit(prior) + 1.5 × (for × diversity − against) ) clamped to [0.02, 0.98]
|
(if the family has no primary item: its strongest derived item alone)
|
||||||
|
for = Σ contribution over supporting families
|
||||||
|
against = Σ contribution over opposing families
|
||||||
|
diversity = 0.6 + 0.4 × min(1, (supporting families − 1) / 2)
|
||||||
|
confidence = sigmoid( logit(prior) + 1.5 × (for × diversity − against) ) clamped to [0.02, 0.98]
|
||||||
```
|
```
|
||||||
|
|
||||||
- The **prior** depends on the epistemic kind: hypothesis 0.25, assumption/opinion 0.4, claim 0.5, observation/memory 0.6, fact 0.8.
|
- The **prior** depends on the epistemic kind: hypothesis 0.25, assumption/opinion 0.4, claim 0.5, observation/memory 0.6, fact 0.8.
|
||||||
- **Diversity** discounts support that all comes from one source, so repetition from a single feed cannot manufacture certainty (threat T4).
|
- **Families** are the unit of independence. Five messages from one person, the episodes they became, the semantic memory consolidated from them and an inference drawn from that memory are all one family: they carry one person's information however many times it has been re-expressed.
|
||||||
|
- **Derivations** say how a row relates to its roots. *Primary* derivations bring information in from outside the belief system: `direct`, `assertion`, `operator`, `external`, `prediction`. *Derived* ones re-express it: `consolidation`, `inference`, `reflection`, `dream`, `retrieval`. Derived rows never add weight to a family that already has a primary row, which is what stops recursive cognition inflating confidence.
|
||||||
|
- **Repetition** within a family is worth a quarter per extra item and never more than half again in total, so a single feed cannot manufacture certainty (threat T4), while three genuinely independent families can.
|
||||||
|
- **Diversity** counts families, not rows.
|
||||||
- Absolute certainty is unreachable; the bounds are deliberate (spec §10).
|
- Absolute certainty is unreachable; the bounds are deliberate (spec §10).
|
||||||
|
|
||||||
The breakdown (prior, for, against, sources, diversity, log-odds, formula, notes) is stored with every version and shown on the belief page.
|
Every evidence row records its `derivation`, `family`, `root_ids` (the originating events or memories) and a `provenance_status`. Rows written before provenance existed are marked `legacy_unknown` with `family = source`; no ancestry is invented for them. The breakdown (prior, per-family contributions, for, against, families, diversity, log-odds, formula, notes) is stored with every version and shown on the belief page under "Independent sources".
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ Beliefs are the mind's propositions about the world, each with a confidence that
|
|||||||
|
|
||||||
## Confidence
|
## Confidence
|
||||||
|
|
||||||
A belief starts from a prior set by its epistemic kind (a hypothesis at 0.25, a claim at 0.50, a fact at 0.80; the full table is under [concepts](concepts)). Each piece of evidence contributes its weight multiplied by the credibility of its source, for or against. Confidence is
|
A belief starts from a prior set by its epistemic kind (a hypothesis at 0.25, a claim at 0.50, a fact at 0.80; the full table is under [concepts](concepts)). Evidence is then grouped by **family**, the independent source a row ultimately rests on: a person, an integration, an operator, a prediction. Within a family the strongest primary item counts in full, each further primary item adds a quarter, and the total is capped at one and a half times the strongest; derived items (a consolidated memory, an inference, a reflection, a dream, a recalled memory) count only when the family has no primary item at all, because they re-express information rather than corroborate it. The confidence is
|
||||||
|
|
||||||
```
|
```
|
||||||
confidence = sigmoid( logit(prior) + 1.5 × (for × diversity − against) )
|
confidence = sigmoid( logit(prior) + 1.5 × (for × diversity − against) )
|
||||||
```
|
```
|
||||||
|
|
||||||
where `diversity` rises from 0.6 for a single source to 1.0 for three or more independent sources, so repetition from one place is discounted. The result is bounded to [0.02, 0.98]: a mind is never certain and never certain of the negation. The breakdown (prior, each evidence row, the diversity factor, the formula) is stored with every version and shown on the belief page.
|
where `for` and `against` are the sums of family contributions and `diversity` rises from 0.6 for a single family to 1.0 for three or more. The result is bounded to [0.02, 0.98]: a mind is never certain and never certain of the negation. Every evidence row carries its derivation, family and root ids; the breakdown, family by family, is stored with every version and shown on the belief page. Rows written before provenance was recorded are marked as legacy and grouped by their source string; no ancestry is invented for them.
|
||||||
|
|
||||||
## Statuses
|
## Statuses
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (s *Server) beliefsCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
in.Weight = 0.7
|
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",
|
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})
|
Alternatives: in.Alternatives})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, r, beliefErr(err))
|
fail(w, r, beliefErr(err))
|
||||||
@@ -235,7 +235,7 @@ func (s *Server) beliefEvidence(w http.ResponseWriter, r *http.Request) {
|
|||||||
if in.Kind == "" {
|
if in.Kind == "" {
|
||||||
in.Kind = "operator"
|
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 {
|
if err != nil {
|
||||||
fail(w, r, beliefErr(err))
|
fail(w, r, beliefErr(err))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func (s *Server) mindImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
weight = 0.5
|
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",
|
_, 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 err != nil {
|
||||||
if errors.Is(err, beliefs.ErrInvalid) || errors.Is(err, beliefs.ErrBlocked) {
|
if errors.Is(err, beliefs.ErrInvalid) || errors.Is(err, beliefs.ErrBlocked) {
|
||||||
problems = append(problems, "belief: "+err.Error())
|
problems = append(problems, "belief: "+err.Error())
|
||||||
|
|||||||
+167
-22
@@ -4,10 +4,13 @@
|
|||||||
package beliefs
|
package beliefs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/andydixon/noema/pkg/concepts"
|
"github.com/andydixon/noema/pkg/concepts"
|
||||||
|
"github.com/andydixon/noema/pkg/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status of a belief.
|
// Status of a belief.
|
||||||
@@ -70,44 +73,186 @@ type Evidence struct {
|
|||||||
Weight float64 `json:"weight"` // how strongly it bears on the proposition, (0,1]
|
Weight float64 `json:"weight"` // how strongly it bears on the proposition, (0,1]
|
||||||
Credibility float64 `json:"credibility"` // trust in the source, [0,1]
|
Credibility float64 `json:"credibility"` // trust in the source, [0,1]
|
||||||
Retracted bool `json:"retracted"`
|
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.
|
// Breakdown explains a confidence value.
|
||||||
type Breakdown struct {
|
type Breakdown struct {
|
||||||
Prior float64 `json:"prior"`
|
Prior float64 `json:"prior"`
|
||||||
For float64 `json:"for"` // Σ weight×credibility, supporting
|
Families []FamilyShare `json:"families,omitempty"` // per independent-source group
|
||||||
Against float64 `json:"against"` // Σ weight×credibility, opposing
|
For float64 `json:"for"` // Σ weight×credibility, supporting
|
||||||
SourcesFor int `json:"sources_for"` // distinct sources supporting
|
Against float64 `json:"against"` // Σ weight×credibility, opposing
|
||||||
DiversityScale float64 `json:"diversity_scale"` // discount when support comes from one source
|
SourcesFor int `json:"sources_for"` // distinct sources supporting
|
||||||
LogOdds float64 `json:"log_odds"`
|
DiversityScale float64 `json:"diversity_scale"` // discount when support comes from one source
|
||||||
Confidence float64 `json:"confidence"`
|
LogOdds float64 `json:"log_odds"`
|
||||||
Formula string `json:"formula"`
|
Confidence float64 `json:"confidence"`
|
||||||
Notes []string `json:"notes,omitempty"`
|
Formula string `json:"formula"`
|
||||||
|
Notes []string `json:"notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute derives confidence from prior and evidence. Support from a single
|
// Compute derives confidence from prior and evidence.
|
||||||
// source is discounted (T4: poisoned memory) so repetition alone cannot make
|
//
|
||||||
// a belief strong.
|
// 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 {
|
func Compute(prior float64, ev []Evidence) Breakdown {
|
||||||
b := Breakdown{Prior: clampP(prior), Formula: "confidence = sigmoid(logit(prior) + slope × (for × diversity − against))"}
|
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)"}
|
||||||
sources := map[string]struct{}{}
|
type group struct {
|
||||||
|
primary, derived []float64
|
||||||
|
items int
|
||||||
|
}
|
||||||
|
groups := map[string]map[string]*group{"for": {}, "against": {}}
|
||||||
|
var order []string
|
||||||
for _, e := range ev {
|
for _, e := range ev {
|
||||||
if e.Retracted || e.Weight <= 0 {
|
if e.Retracted || e.Weight <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
v := clamp01(e.Weight) * clamp01(e.Credibility)
|
v := clamp01(e.Weight) * clamp01(e.Credibility)
|
||||||
|
dir := "for"
|
||||||
if e.Direction == "against" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
b.For += v
|
b.For += c
|
||||||
key := e.Source
|
|
||||||
if key == "" {
|
|
||||||
key = e.Kind + ":" + e.RefID
|
|
||||||
}
|
|
||||||
sources[key] = struct{}{}
|
|
||||||
}
|
}
|
||||||
b.SourcesFor = len(sources)
|
b.SourcesFor = len(groups["for"])
|
||||||
// One source: 60% of face value; three or more independent sources: full.
|
// One source: 60% of face value; three or more independent sources: full.
|
||||||
b.DiversityScale = 1
|
b.DiversityScale = 1
|
||||||
if b.SourcesFor > 0 {
|
if b.SourcesFor > 0 {
|
||||||
|
|||||||
@@ -67,3 +67,61 @@ func TestContradicts(t *testing.T) {
|
|||||||
t.Fatal("identical propositions flagged")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ type EvidenceInput struct {
|
|||||||
Weight float64
|
Weight float64
|
||||||
Credibility float64
|
Credibility float64
|
||||||
Note string
|
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.
|
// Result is a belief with its computed breakdown and the events written.
|
||||||
@@ -228,12 +233,29 @@ func normalise(e EvidenceInput) Evidence {
|
|||||||
if e.Kind == "" {
|
if e.Kind == "" {
|
||||||
e.Kind = "note"
|
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 {
|
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)`,
|
if e.Derivation == "" {
|
||||||
ids.New(), b.ID, b.MindID, e.Direction, e.Kind, e.RefID, e.Source, e.Weight, e.Credibility, note, nullID(eventID))
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +371,7 @@ func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids
|
|||||||
if q == nil {
|
if q == nil {
|
||||||
q = s.db
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -358,7 +380,7 @@ func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r EvidenceRow
|
var r EvidenceRow
|
||||||
var evID *string
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if evID != nil {
|
if evID != nil {
|
||||||
@@ -519,10 +541,10 @@ func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Each belief becomes counter-evidence for the other, weighted by the other's confidence.
|
// 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
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, x := range []Belief{b, o} {
|
for _, x := range []Belief{b, o} {
|
||||||
|
|||||||
@@ -66,13 +66,19 @@ func (h Handlers) hypothesis(ctx context.Context, c *brain.Cycle, t workspace.Th
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cred := 0.6
|
cred := 0.6
|
||||||
|
family := "event:" + ref.ID
|
||||||
if e, err := c.Runtime.Events().Get(ctx, ids.ID(ref.ID)); err == nil {
|
if e, err := c.Runtime.Events().Get(ctx, ids.ID(ref.ID)); err == nil {
|
||||||
var p events.ObservationPayload
|
var p events.ObservationPayload
|
||||||
if json.Unmarshal(e.Payload, &p) == nil && p.Trust.Valid() {
|
if json.Unmarshal(e.Payload, &p) == nil && p.Trust.Valid() {
|
||||||
cred = p.Trust.Credibility()
|
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",
|
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,
|
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,
|
||||||
|
|||||||
@@ -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 {
|
if bid, ok := t.Content["belief_id"].(string); ok && bid != "" && h.Beliefs != nil {
|
||||||
switch check {
|
switch check {
|
||||||
case "insufficient_evidence", "single_source", "fluent_without_evidence":
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thou
|
|||||||
w += 0.2
|
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,
|
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 {
|
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"}})
|
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
|
continue
|
||||||
|
|||||||
@@ -307,7 +307,19 @@ func (s *Service) consolidate(ctx context.Context, m minds.Mind, c Cluster, llmB
|
|||||||
var refs []events.EvidenceRef
|
var refs []events.EvidenceRef
|
||||||
var epIDs []ids.ID
|
var epIDs []ids.ID
|
||||||
for _, e := range c.Episodes {
|
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"})
|
refs = append(refs, events.EvidenceRef{Kind: "memory", ID: string(e.ID), Note: "episode"})
|
||||||
epIDs = append(epIDs, e.ID)
|
epIDs = append(epIDs, e.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,8 +386,10 @@ func (s *Service) hypothesise(ctx context.Context, m minds.Mind, dream ids.ID, p
|
|||||||
return out[0], nil
|
return out[0], nil
|
||||||
}
|
}
|
||||||
ev := []beliefs.EvidenceInput{
|
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.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)},
|
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}
|
detail := map[string]any{"dream_id": dream, "wording": w.Wording, "rationale": w.Rationale, "shared": p.Shared}
|
||||||
if !w.CallID.IsZero() {
|
if !w.CallID.IsZero() {
|
||||||
@@ -517,3 +519,22 @@ func excerpt(s string, n int) string {
|
|||||||
}
|
}
|
||||||
return s
|
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)}
|
||||||
|
}
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ func (r *Resolver) ResolveIn(ctx context.Context, q storage.Querier, p Predictio
|
|||||||
evs = append(evs, out[0])
|
evs = append(evs, out[0])
|
||||||
return out[0], nil
|
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)
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ package replay
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/andydixon/noema/internal/beliefs"
|
||||||
|
|
||||||
evstore "github.com/andydixon/noema/internal/events"
|
evstore "github.com/andydixon/noema/internal/events"
|
||||||
"github.com/andydixon/noema/internal/minds"
|
"github.com/andydixon/noema/internal/minds"
|
||||||
"github.com/andydixon/noema/internal/replay/replaytest"
|
"github.com/andydixon/noema/internal/replay/replaytest"
|
||||||
"github.com/andydixon/noema/pkg/events"
|
"github.com/andydixon/noema/pkg/events"
|
||||||
|
"github.com/andydixon/noema/pkg/ids"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReplayIsExactWithoutModel(t *testing.T) {
|
func TestReplayIsExactWithoutModel(t *testing.T) {
|
||||||
@@ -51,6 +56,11 @@ func TestReplayIsExactWithoutModel(t *testing.T) {
|
|||||||
if err != nil || sandbox.Status != minds.StatusDisabled || sandbox.ForkedFromSnapshot == nil {
|
if err != nil || sandbox.Status != minds.StatusDisabled || sandbox.ForkedFromSnapshot == nil {
|
||||||
t.Fatalf("sandbox mind wrong: %+v %v", sandbox, err)
|
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.
|
// The source's history is untouched by the replay.
|
||||||
after, _ := h.Events.Query(ctx, evstore.Filter{MindID: src.ID, Limit: 1})
|
after, _ := h.Events.Query(ctx, evstore.Filter{MindID: src.ID, Limit: 1})
|
||||||
if after[0].Seq != last[0].Seq {
|
if after[0].Seq != last[0].Seq {
|
||||||
@@ -62,3 +72,27 @@ func TestReplayIsExactWithoutModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_ = events.Observation
|
_ = 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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
DROP INDEX IF EXISTS belief_evidence_family_idx;
|
||||||
|
ALTER TABLE belief_evidence
|
||||||
|
DROP COLUMN IF EXISTS provenance_status,
|
||||||
|
DROP COLUMN IF EXISTS root_ids,
|
||||||
|
DROP COLUMN IF EXISTS family,
|
||||||
|
DROP COLUMN IF EXISTS derivation;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Evidence provenance (review pass §7.2 item 2).
|
||||||
|
-- family: the independent-source key evidence is grouped by when confidence
|
||||||
|
-- is computed (an actor, an integration, an operator, a prediction).
|
||||||
|
-- derivation: how the row relates to its roots (direct, assertion, operator,
|
||||||
|
-- external, prediction, consolidation, inference, reflection, dream,
|
||||||
|
-- retrieval). Derived rows never add weight to a family that already
|
||||||
|
-- has a primary member.
|
||||||
|
-- root_ids: the originating event or memory ids this row ultimately rests on.
|
||||||
|
-- provenance_status: 'recorded' for rows written with ancestry; existing rows
|
||||||
|
-- are marked 'legacy_unknown' with family = source, never invented.
|
||||||
|
ALTER TABLE belief_evidence
|
||||||
|
ADD COLUMN IF NOT EXISTS derivation text NOT NULL DEFAULT 'legacy',
|
||||||
|
ADD COLUMN IF NOT EXISTS family text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN IF NOT EXISTS root_ids uuid[] NOT NULL DEFAULT '{}',
|
||||||
|
ADD COLUMN IF NOT EXISTS provenance_status text NOT NULL DEFAULT 'legacy_unknown'
|
||||||
|
CHECK (provenance_status IN ('recorded','legacy_unknown'));
|
||||||
|
UPDATE belief_evidence SET family = COALESCE(NULLIF(source, ''), kind || ':' || ref_id) WHERE family = '';
|
||||||
|
CREATE INDEX IF NOT EXISTS belief_evidence_family_idx ON belief_evidence (belief_id, family);
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
<div class="grid cols-2">
|
<div class="grid cols-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Evidence for</h2>
|
<h2>Evidence for</h2>
|
||||||
<ul class="plain ev">{{range $d.Evidence}}{{if eq .Direction "for"}}<li class="{{if .Retracted}}retracted{{end}}"><div><strong>{{conf .Weight}}</strong> × cred {{conf .Credibility}} <span class="badge">{{.Kind}}</span> {{if .Retracted}}<span class="badge danger">retracted</span>{{end}}</div><div>{{.Note}}</div><div class="muted small">{{.Source}} · {{ago .CreatedAt}} {{if and (eq .Kind "event") .RefID}}· <a href="/events/{{.RefID}}">event</a>{{else if and (eq .Kind "memory") .RefID}}· <a href="/minds/{{$m.Slug}}/memory/{{.RefID}}">memory</a>{{else if and (eq .Kind "belief") .RefID}}· <a href="/minds/{{$m.Slug}}/beliefs/{{.RefID}}">belief</a>{{end}}</div></li>{{end}}{{else}}<li class="muted">None.</li>{{end}}</ul>
|
<ul class="plain ev">{{range $d.Evidence}}{{if eq .Direction "for"}}<li class="{{if .Retracted}}retracted{{end}}"><div><strong>{{conf .Weight}}</strong> × cred {{conf .Credibility}} <span class="badge">{{.Kind}}</span> {{if .Retracted}}<span class="badge danger">retracted</span>{{end}}</div><div>{{.Note}}</div><div class="muted small">{{.Source}} · {{ago .CreatedAt}} {{if and (eq .Kind "event") .RefID}}· <a href="/events/{{.RefID}}">event</a>{{else if and (eq .Kind "memory") .RefID}}· <a href="/minds/{{$m.Slug}}/memory/{{.RefID}}">memory</a>{{else if and (eq .Kind "belief") .RefID}}· <a href="/minds/{{$m.Slug}}/beliefs/{{.RefID}}">belief</a>{{end}}</div><div class="muted small">provenance: <span class="badge small">{{.Derivation}}</span> family <code>{{.Family}}</code>{{if eq .ProvenanceStatus "legacy_unknown"}} <span class="badge warn small" title="written before ancestry was recorded; family is the source string">legacy</span>{{end}}{{range .RootIDs}} · <a href="/events/{{.}}">root</a>{{end}}</div></li>{{end}}{{else}}<li class="muted">None.</li>{{end}}</ul>
|
||||||
|
{{if $bd.Families}}<h3 class="small muted">Independent sources</h3><table class="table compact"><thead><tr><th>Family</th><th></th><th class="num">Items</th><th class="num">Primary</th><th class="num">Counts as</th></tr></thead><tbody>{{range $bd.Families}}<tr><td><code>{{.Family}}</code></td><td class="small">{{.Direction}}</td><td class="num">{{.Items}}</td><td class="num">{{.Primary}}</td><td class="num">{{printf "%.2f" .Contribution}}</td></tr>{{end}}</tbody></table><p class="small muted">Within a family the strongest primary item counts in full and further items add a quarter each (capped at 1.5×); derived items count only when the family has no primary item. Diversity counts families: {{$bd.SourcesFor}}.</p>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Evidence against</h2>
|
<h2>Evidence against</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user