Files
noema/internal/beliefs/store.go
T
Andy Dixon 4a7dafc71f feat(agency): forensic "why", attribution and introspection accuracy
- agency.Explain answers "why did this happen?" for an execution,
  approval, capability request, plan step, intention or event from
  recorded state only: action, immediate plan step, plan, intention and
  any subordinate intention, originating wants, goal, supporting beliefs
  and their evidence, conflicting wants with their resolution,
  commitments applied, formation and reconsideration decisions, the
  expectation and its outcome, and explicit gaps where nothing is
  recorded. Hidden model reasoning is never shown because it is not
  stored.
- agency.Attribute classifies an outcome as caused (a recorded causal
  link from one of the mind's actions), followed (the mind acted on the
  same target earlier with no recorded link), independent or unknown;
  temporal order alone is never taken for causation.
- New introspection intents answer from records: why an action happened,
  what was believed at a past time (from append-only belief versions),
  which earlier state is the mind's (lineage), what would have happened
  without an event (only from completed counterfactual experiments,
  otherwise "no evidence"), how the mind changed, and what it wants,
  intends and has committed to.
- research.ScoreIntrospection compares a reply with the recorded facts
  sentence by sentence (supported, partially supported, unsupported,
  contradicted, unknown). Fluent reasons with no recorded counterpart earn
  nothing, and a claim that an action was done is checked against
  execution records. Every reply is scored; results are append-only
  research measurements (migration 0028), and failures raise
  unsupported_explanation or action_claim_without_record warnings.
2026-09-16 23:53:06 +01:00

1059 lines
41 KiB
Go

package beliefs
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/andydixon/noema/internal/storage"
"github.com/andydixon/noema/pkg/concepts"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// Belief is a proposition with derived confidence and status.
type Belief struct {
ID ids.ID `json:"id"`
MindID ids.ID `json:"mind_id"`
Proposition string `json:"proposition"`
Subject string `json:"subject"`
Epistemic concepts.Epistemic `json:"epistemic"`
Status Status `json:"status"`
Confidence float64 `json:"confidence"`
Prior float64 `json:"prior"`
Version int `json:"version"`
Source string `json:"source"`
Detail map[string]any `json:"detail"`
OriginEvent ids.ID `json:"origin_event,omitempty"`
SupersededBy ids.ID `json:"superseded_by,omitempty"`
PinnedStatus *Status `json:"pinned_status,omitempty"`
FirstCreated time.Time `json:"first_created"`
LastEvaluated time.Time `json:"last_evaluated"`
Evaluations int `json:"evaluations"`
// Temporal scope and validity (see temporal.go). ValidUntil is set when a
// later observation superseded a transient state; the belief then says
// what was true, and when.
TemporalScope string `json:"temporal_scope"`
ObservedAt *time.Time `json:"observed_at,omitempty"`
ValidFrom *time.Time `json:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty"`
RedactedAt *time.Time `json:"redacted_at,omitempty"`
}
// EvidenceRow is stored evidence.
type EvidenceRow struct {
ID ids.ID `json:"id"`
Evidence `json:",inline"`
Note string `json:"note"`
EventID ids.ID `json:"event_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// Alternative is a competing explanation kept alongside a belief.
type Alternative struct {
ID ids.ID `json:"id"`
Proposition string `json:"proposition"`
Confidence float64 `json:"confidence"`
Note string `json:"note"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// Version is one entry in the append-only history.
type Version struct {
Version int `json:"version"`
Status Status `json:"status"`
Confidence float64 `json:"confidence"`
Proposition string `json:"proposition"`
Reason string `json:"reason"`
Breakdown Breakdown `json:"breakdown"`
EventID ids.ID `json:"event_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// Link relates two beliefs.
type Link struct {
ID ids.ID `json:"id"`
FromID ids.ID `json:"from_id"`
ToID ids.ID `json:"to_id"`
Relation string `json:"relation"`
Confidence float64 `json:"confidence"`
Other *Belief `json:"other,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// Correction is an operator statement about the mind's content.
type Correction struct {
ID ids.ID `json:"id"`
Kind string `json:"kind"`
TargetKind string `json:"target_kind"`
TargetID string `json:"target_id"`
Statement string `json:"statement"`
Reason string `json:"reason"`
CreatedBy string `json:"created_by"`
EventID ids.ID `json:"event_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// ErrInvalid marks bad input; ErrBlocked means a do-not-infer correction applies.
var (
ErrInvalid = errors.New("invalid belief")
ErrBlocked = errors.New("blocked by operator correction")
)
// Emitter appends events inside the caller's transaction and returns them.
// The brain cycle and the API each provide one so provenance events are
// written atomically with belief rows.
type Emitter func(ctx context.Context, q storage.Querier, n events.New) (events.Event, error)
// Store persists beliefs.
type Store struct {
db *pgxpool.Pool
}
// NewStore creates a store.
func NewStore(db *pgxpool.Pool) *Store { return &Store{db: db} }
// New describes a belief to create.
type New struct {
MindID ids.ID
Proposition string
Subject string
Epistemic concepts.Epistemic
Source string
Evidence []EvidenceInput
Alternatives []string
Detail map[string]any
OriginEvent ids.ID
Correlation ids.ID
Causation ids.ID
// ObservedAt is when the state described was observed (defaults to now);
// TemporalScope overrides the deterministic classifier when the producer knows.
ObservedAt *time.Time
TemporalScope string
}
// EvidenceInput is evidence supplied by callers.
type EvidenceInput struct {
Direction string
Kind string
RefID string
Source string
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.
type Result struct {
Belief Belief
Breakdown Breakdown
Events []events.Event
}
// Create stores a belief, its evidence, alternatives and first version, emits
// belief_created/hypothesis_created, and runs contradiction detection.
func (s *Store) Create(ctx context.Context, q storage.Querier, emit Emitter, n New) (Result, error) {
n.Proposition = strings.TrimSpace(n.Proposition)
if n.MindID.IsZero() || n.Proposition == "" || len(n.Proposition) > 2000 {
return Result{}, fmt.Errorf("%w: proposition required (max 2000 chars)", ErrInvalid)
}
if !n.Epistemic.Valid() {
n.Epistemic = concepts.Hypothesis
}
if blocked, c := s.blocked(ctx, q, n.MindID, n.Proposition); blocked {
return Result{}, fmt.Errorf("%w: %s", ErrBlocked, c.Statement)
}
var ev []Evidence
var notes []string
for _, e := range n.Evidence {
ne := normalise(e)
ne, note := s.applyReliability(ctx, q, n.MindID, strings.TrimSpace(n.Subject), ne, e.Note)
ev = append(ev, ne)
notes = append(notes, note)
}
prior := PriorFor(n.Epistemic)
bd := Compute(prior, ev)
st := Derive(bd, n.Epistemic)
observed := time.Now()
if n.ObservedAt != nil {
observed = *n.ObservedAt
}
scope := n.TemporalScope
if scope == "" {
scope = ClassifyScope(n.Proposition)
}
b := Belief{ID: ids.New(), MindID: n.MindID, Proposition: n.Proposition, Subject: strings.TrimSpace(n.Subject), Epistemic: n.Epistemic, Status: st, Confidence: bd.Confidence,
Prior: prior, Version: 1, Source: n.Source, Detail: n.Detail, OriginEvent: n.OriginEvent, FirstCreated: time.Now(), LastEvaluated: time.Now(), Evaluations: 1,
TemporalScope: scope, ObservedAt: &observed}
if scope == ScopeTransient {
b.ValidFrom = &observed
}
if b.Detail == nil {
b.Detail = map[string]any{}
}
detail, _ := json.Marshal(b.Detail)
if _, err := q.Exec(ctx, `INSERT INTO beliefs (id, mind_id, proposition, subject, epistemic, status, confidence, prior, version, source, detail, origin_event, temporal_scope, observed_at, valid_from)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$10,$11,$12,$13,$14)`, b.ID, b.MindID, b.Proposition, b.Subject, string(b.Epistemic), string(b.Status), b.Confidence, b.Prior, b.Source, detail, nullID(b.OriginEvent), b.TemporalScope, b.ObservedAt, b.ValidFrom); err != nil {
return Result{}, err
}
typ := events.BeliefCreated
if n.Epistemic == concepts.Hypothesis {
typ = events.HypothesisCreated
}
refs := []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}}
for _, e := range n.Evidence {
if e.RefID != "" {
refs = append(refs, events.EvidenceRef{Kind: e.Kind, ID: e.RefID, Note: e.Direction + ": " + e.Note})
}
}
created, err := emit(ctx, q, events.New{MindID: b.MindID, Type: typ, Source: n.Source, Summary: fmt.Sprintf("%s (%.2f, %s): %s", typ, b.Confidence, b.Status, b.Proposition),
Salience: 0.5 + 0.3*b.Confidence, Confidence: b.Confidence, CausationID: n.Causation, CorrelationID: n.Correlation, Evidence: refs,
Metadata: map[string]any{"belief_id": string(b.ID), "status": b.Status, "subject": b.Subject}, Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "status": b.Status, "confidence": b.Confidence, "breakdown": bd, "alternatives": n.Alternatives}})
if err != nil {
return Result{}, err
}
res := Result{Belief: b, Breakdown: bd, Events: []events.Event{created}}
for i, e := range ev {
if err := s.insertEvidence(ctx, q, b, e, notes[i], created.ID); err != nil {
return Result{}, err
}
}
for _, alt := range n.Alternatives {
if alt = strings.TrimSpace(alt); alt != "" {
if _, err := q.Exec(ctx, `INSERT INTO belief_alternatives (id, belief_id, mind_id, proposition, confidence) VALUES ($1,$2,$3,$4,$5)`, ids.New(), b.ID, b.MindID, alt, 0.2); err != nil {
return Result{}, err
}
}
}
if err := s.insertVersion(ctx, q, b, bd, "created", created.ID); err != nil {
return Result{}, err
}
contra, err := s.detectContradictions(ctx, q, emit, b, created.ID)
if err != nil {
return Result{}, err
}
res.Events = append(res.Events, contra...)
return res, nil
}
func normalise(e EvidenceInput) Evidence {
if e.Direction != "against" {
e.Direction = "for"
}
if e.Weight <= 0 || e.Weight > 1 {
e.Weight = 0.5
}
if e.Credibility <= 0 || e.Credibility > 1 {
e.Credibility = 0.5
}
if e.Kind == "" {
e.Kind = "note"
}
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 {
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
}
func (s *Store) insertVersion(ctx context.Context, q storage.Querier, b Belief, bd Breakdown, reason string, eventID ids.ID) error {
raw, _ := json.Marshal(bd)
_, err := q.Exec(ctx, `INSERT INTO belief_versions (id, belief_id, mind_id, version, status, confidence, proposition, reason, breakdown, event_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
ids.New(), b.ID, b.MindID, b.Version, string(b.Status), b.Confidence, b.Proposition, reason, raw, nullID(eventID))
return err
}
func nullID(id ids.ID) *string {
if id.IsZero() {
return nil
}
s := string(id)
return &s
}
const cols = `b.id, b.mind_id, b.proposition, b.subject, b.epistemic, b.status, b.confidence, b.prior, b.version, b.source, b.detail, b.origin_event, b.superseded_by, b.pinned_status, b.first_created, b.last_evaluated, b.evaluations, b.temporal_scope, b.observed_at, b.valid_from, b.valid_until, b.redacted_at`
func scan(row pgx.Row) (Belief, error) {
var b Belief
var epi, status string
var detail []byte
var origin, superseded, pinned *string
if err := row.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil, &b.RedactedAt); err != nil {
return b, storage.Translate(err)
}
b.Epistemic, b.Status = concepts.Epistemic(epi), Status(status)
json.Unmarshal(detail, &b.Detail) //nolint:errcheck
if b.Detail == nil {
b.Detail = map[string]any{}
}
if origin != nil {
b.OriginEvent = ids.ID(*origin)
}
if superseded != nil {
b.SupersededBy = ids.ID(*superseded)
}
if pinned != nil {
st := Status(*pinned)
b.PinnedStatus = &st
}
return b, nil
}
// Get fetches a belief scoped to a mind. A nil q reads from the pool.
func (s *Store) Get(ctx context.Context, q storage.Querier, mind, id ids.ID) (Belief, error) {
if q == nil {
q = s.db
}
return scan(q.QueryRow(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.id=$1 AND b.mind_id=$2`, id, mind))
}
// Filter selects beliefs.
type Filter struct {
Statuses []Status
MinConfidence float64
MaxConfidence float64
Subject string
Search string
Limit int
}
// List returns beliefs by confidence descending.
func (s *Store) List(ctx context.Context, mind ids.ID, f Filter) ([]Belief, error) {
where := []string{"b.mind_id = $1"}
args := []any{mind}
arg := func(v any) string { args = append(args, v); return fmt.Sprintf("$%d", len(args)) }
if len(f.Statuses) > 0 {
ss := make([]string, len(f.Statuses))
for i, st := range f.Statuses {
ss[i] = string(st)
}
where = append(where, "b.status = ANY("+arg(ss)+")")
} else {
where = append(where, "b.status <> 'superseded'")
}
if f.MinConfidence > 0 {
where = append(where, "b.confidence >= "+arg(f.MinConfidence))
}
if f.MaxConfidence > 0 {
where = append(where, "b.confidence <= "+arg(f.MaxConfidence))
}
if f.Subject != "" {
where = append(where, "b.subject = "+arg(f.Subject))
}
if q := strings.TrimSpace(f.Search); q != "" {
where = append(where, "b.search @@ websearch_to_tsquery('english', "+arg(q)+")")
}
limit := f.Limit
if limit <= 0 || limit > 500 {
limit = 100
}
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE `+strings.Join(where, " AND ")+` ORDER BY b.confidence DESC, b.last_evaluated DESC LIMIT `+arg(limit), args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Belief
for rows.Next() {
b, err := scan(rows)
if err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
// EvidenceFor returns all evidence rows of a belief. A nil q reads from the pool.
func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids.ID) ([]EvidenceRow, error) {
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, 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
}
defer rows.Close()
var out []EvidenceRow
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, &r.Derivation, &r.Family, &r.RootIDs, &r.ProvenanceStatus); err != nil {
return nil, err
}
if evID != nil {
r.EventID = ids.ID(*evID)
}
out = append(out, r)
}
return out, rows.Err()
}
// AddEvidence attaches evidence, recomputes confidence and status, appends a
// version when anything changed and emits the matching event.
func (s *Store) AddEvidence(ctx context.Context, q storage.Querier, emit Emitter, mind, id ids.ID, in EvidenceInput, reason string, causation, correlation ids.ID) (Result, error) {
b, err := s.Get(ctx, q, mind, id)
if err != nil {
return Result{}, err
}
if b.Status == StatusSuperseded {
return Result{}, fmt.Errorf("%w: belief is superseded", ErrInvalid)
}
e := normalise(in)
e, note := s.applyReliability(ctx, q, mind, b.Subject, e, in.Note)
if err := s.insertEvidence(ctx, q, b, e, note, ids.Nil); err != nil {
return Result{}, err
}
return s.reevaluate(ctx, q, emit, b, reason, causation, correlation, &e, in)
}
// Reevaluate recomputes a belief from its stored evidence (used after
// retractions and corrections).
func (s *Store) Reevaluate(ctx context.Context, q storage.Querier, emit Emitter, mind, id ids.ID, reason string) (Result, error) {
b, err := s.Get(ctx, q, mind, id)
if err != nil {
return Result{}, err
}
return s.reevaluate(ctx, q, emit, b, reason, ids.Nil, ids.Nil, nil, EvidenceInput{})
}
func (s *Store) reevaluate(ctx context.Context, q storage.Querier, emit Emitter, b Belief, reason string, causation, correlation ids.ID, added *Evidence, in EvidenceInput) (Result, error) {
rows, err := s.EvidenceFor(ctx, q, b.MindID, b.ID)
if err != nil {
return Result{}, err
}
ev := make([]Evidence, 0, len(rows))
for _, r := range rows {
ev = append(ev, r.Evidence)
}
bd := Compute(b.Prior, ev)
st := Derive(bd, b.Epistemic)
if b.PinnedStatus != nil {
st = *b.PinnedStatus
}
old := b
b.Confidence, b.Status, b.LastEvaluated, b.Evaluations = bd.Confidence, st, time.Now(), b.Evaluations+1
changed := old.Confidence != b.Confidence || old.Status != b.Status
res := Result{Belief: b, Breakdown: bd}
typ := events.BeliefUpdated
summary := fmt.Sprintf("belief re-evaluated (%.2f → %.2f, %s): %s", old.Confidence, b.Confidence, b.Status, b.Proposition)
if added != nil {
if added.Direction == "for" {
typ = events.HypothesisSupported
if b.Epistemic != concepts.Hypothesis {
typ = events.BeliefUpdated
}
summary = fmt.Sprintf("support added (%.2f → %.2f): %s", old.Confidence, b.Confidence, b.Proposition)
} else {
typ = events.HypothesisWeakened
if b.Epistemic != concepts.Hypothesis {
typ = events.BeliefChallenged
}
summary = fmt.Sprintf("counter-evidence added (%.2f → %.2f): %s", old.Confidence, b.Confidence, b.Proposition)
}
}
refs := []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}}
if added != nil && added.RefID != "" {
refs = append(refs, events.EvidenceRef{Kind: added.Kind, ID: added.RefID, Note: added.Direction + ": " + in.Note})
}
ev1, err := emit(ctx, q, events.New{MindID: b.MindID, Type: typ, Source: firstNonEmpty(in.Source, "beliefs"), Summary: summary, Salience: 0.3 + 0.4*absf(b.Confidence-old.Confidence)*4, Confidence: b.Confidence,
CausationID: causation, CorrelationID: correlation, Evidence: refs,
Metadata: map[string]any{"belief_id": string(b.ID), "status": b.Status, "from": old.Confidence, "to": b.Confidence},
Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "from": map[string]any{"confidence": old.Confidence, "status": old.Status}, "to": map[string]any{"confidence": b.Confidence, "status": b.Status}, "breakdown": bd, "reason": reason}})
if err != nil {
return Result{}, err
}
res.Events = append(res.Events, ev1)
if changed {
b.Version++
res.Belief = b
if err := s.insertVersion(ctx, q, b, bd, reason, ev1.ID); err != nil {
return Result{}, err
}
}
if _, err := q.Exec(ctx, `UPDATE beliefs SET confidence=$3, status=$4, version=$5, last_evaluated=now(), evaluations=evaluations+1 WHERE id=$1 AND mind_id=$2`, b.ID, b.MindID, b.Confidence, string(b.Status), b.Version); err != nil {
return Result{}, err
}
if changed && added != nil && added.Direction == "for" {
contra, err := s.detectContradictions(ctx, q, emit, b, ev1.ID)
if err != nil {
return Result{}, err
}
res.Events = append(res.Events, contra...)
}
return res, nil
}
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
func absf(v float64) float64 {
if v < 0 {
return -v
}
return v
}
// detectContradictions compares b against the mind's active beliefs. A
// contradiction links both, emits an event and adds mutual counter-evidence
// so both confidences reflect the conflict until it is resolved.
func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emit Emitter, b Belief, causation ids.ID) ([]events.Event, error) {
rows, err := q.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.id<>$2 AND b.status NOT IN ('rejected','superseded') AND ($3 = '' OR b.subject = $3 OR b.subject = '') LIMIT 500`, b.MindID, b.ID, b.Subject)
if err != nil {
return nil, err
}
var others []Belief
for rows.Next() {
o, err := scan(rows)
if err != nil {
rows.Close()
return nil, err
}
others = append(others, o)
}
rows.Close()
var out []events.Event
for _, o := range others {
ok, sim := Contradicts(b.Proposition, o.Proposition)
if !ok {
continue
}
var exists bool
q.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM belief_links WHERE relation='contradicts' AND ((from_id=$1 AND to_id=$2) OR (from_id=$2 AND to_id=$1)))`, b.ID, o.ID).Scan(&exists) //nolint:errcheck
if exists {
continue
}
if StateChange(o, b) {
// A later observation of a transient state: the older belief was
// true of its time and is closed, not contested and not weakened.
ev, err := s.closeState(ctx, q, emit, o, b, sim, causation)
if err != nil {
return nil, err
}
out = append(out, ev)
continue
}
ev, err := emit(ctx, q, events.New{MindID: b.MindID, Type: events.ContradictionDetected, Source: "beliefs.contradiction", Summary: fmt.Sprintf("contradiction: %q vs %q", trunc(b.Proposition, 60), trunc(o.Proposition, 60)),
Salience: 0.8, Confidence: sim, CausationID: causation,
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}, {Kind: "belief", ID: string(o.ID)}},
Metadata: map[string]any{"belief_id": string(b.ID), "other_id": string(o.ID), "similarity": sim},
Payload: map[string]any{"a": map[string]any{"id": b.ID, "proposition": b.Proposition, "confidence": b.Confidence}, "b": map[string]any{"id": o.ID, "proposition": o.Proposition, "confidence": o.Confidence}, "similarity": sim, "method": "negation+jaccard"}})
if err != nil {
return nil, err
}
out = append(out, ev)
if _, err := q.Exec(ctx, `INSERT INTO belief_links (id, mind_id, from_id, to_id, relation, confidence, event_id) VALUES ($1,$2,$3,$4,'contradicts',$5,$6) ON CONFLICT DO NOTHING`, ids.New(), b.MindID, b.ID, o.ID, sim, ev.ID); err != nil {
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, 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, 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} {
rows, err := s.EvidenceFor(ctx, q, x.MindID, x.ID)
if err != nil {
return nil, err
}
var evs []Evidence
for _, r := range rows {
evs = append(evs, r.Evidence)
}
bd := Compute(x.Prior, evs)
st := Derive(bd, x.Epistemic)
if x.PinnedStatus != nil {
st = *x.PinnedStatus
}
if bd.Confidence != x.Confidence || st != x.Status {
x.Confidence, x.Status, x.Version = bd.Confidence, st, x.Version+1
if err := s.insertVersion(ctx, q, x, bd, "contradiction detected", ev.ID); err != nil {
return nil, err
}
if _, err := q.Exec(ctx, `UPDATE beliefs SET confidence=$3, status=$4, version=$5, last_evaluated=now() WHERE id=$1 AND mind_id=$2`, x.ID, x.MindID, x.Confidence, string(x.Status), x.Version); err != nil {
return nil, err
}
}
}
}
return out, nil
}
// Retract is the operator saying "this belief is wrong". Status is pinned to
// rejected, a correction is recorded, and optionally a do-not-infer rule.
func (s *Store) Retract(ctx context.Context, emit Emitter, mind, id ids.ID, reason, by string, doNotInfer bool) (Result, error) {
var res Result
err := storage.Tx(ctx, s.db, func(tx pgx.Tx) error {
b, err := s.Get(ctx, tx, mind, id)
if err != nil {
return err
}
// An operator saying "wrong" is an outcome for the sources that backed
// it, in this belief's domain only: nobody's standing elsewhere moves.
if err := s.RecordBeliefOutcome(ctx, tx, mind, id, false); err != nil {
return err
}
ev, err := emit(ctx, tx, events.New{MindID: mind, Type: events.BeliefRetracted, Source: "operator", Summary: fmt.Sprintf("operator %s rejected belief: %s (%s)", by, b.Proposition, reason),
Salience: 0.7, Confidence: 1, Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}},
Metadata: map[string]any{"belief_id": string(b.ID), "actor": by, "do_not_infer": doNotInfer}, Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "reason": reason, "previous": map[string]any{"confidence": b.Confidence, "status": b.Status}}})
if err != nil {
return err
}
if err := s.insertEvidence(ctx, tx, b, Evidence{Direction: "against", Kind: "operator", RefID: string(ev.ID), Source: "operator", Weight: 1, Credibility: 0.95}, reason, ev.ID); err != nil {
return err
}
rej := StatusRejected
b.PinnedStatus, b.Status, b.Version = &rej, rej, b.Version+1
rows, _ := s.EvidenceFor(ctx, tx, mind, id)
var evs []Evidence
for _, r := range rows {
evs = append(evs, r.Evidence)
}
bd := Compute(b.Prior, evs)
b.Confidence = bd.Confidence
if err := s.insertVersion(ctx, tx, b, bd, "operator correction: "+reason, ev.ID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE beliefs SET status='rejected', pinned_status='rejected', confidence=$3, version=$4, last_evaluated=now() WHERE id=$1 AND mind_id=$2`, id, mind, b.Confidence, b.Version); err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,'belief_wrong','belief',$3,$4,$5,$6,$7)`, ids.New(), mind, id, b.Proposition, reason, by, ev.ID); err != nil {
return err
}
if doNotInfer {
if _, err := tx.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,'do_not_infer','belief',$3,$4,$5,$6,$7)`, ids.New(), mind, id, b.Proposition, reason, by, ev.ID); err != nil {
return err
}
}
res = Result{Belief: b, Breakdown: bd, Events: []events.Event{ev}}
return nil
})
return res, err
}
// AddCorrection records a free-standing correction such as "do not infer X".
func (s *Store) AddCorrection(ctx context.Context, q storage.Querier, mind ids.ID, c Correction) (Correction, error) {
c.ID, c.CreatedAt = ids.New(), time.Now()
_, err := q.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
c.ID, mind, c.Kind, c.TargetKind, c.TargetID, c.Statement, c.Reason, c.CreatedBy, nullID(c.EventID))
return c, err
}
// Corrections lists corrections for a mind, newest first.
func (s *Store) Corrections(ctx context.Context, mind ids.ID, kind string) ([]Correction, error) {
rows, err := s.db.Query(ctx, `SELECT id, kind, target_kind, target_id, statement, reason, created_by, event_id, created_at FROM corrections WHERE mind_id=$1 AND ($2='' OR kind=$2) ORDER BY created_at DESC LIMIT 500`, mind, kind)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Correction
for rows.Next() {
var c Correction
var ev *string
if err := rows.Scan(&c.ID, &c.Kind, &c.TargetKind, &c.TargetID, &c.Statement, &c.Reason, &c.CreatedBy, &ev, &c.CreatedAt); err != nil {
return nil, err
}
if ev != nil {
c.EventID = ids.ID(*ev)
}
out = append(out, c)
}
return out, rows.Err()
}
// blocked checks do-not-infer corrections against a proposition.
func (s *Store) blocked(ctx context.Context, q storage.Querier, mind ids.ID, proposition string) (bool, Correction) {
rows, err := q.Query(ctx, `SELECT id, statement, reason FROM corrections WHERE mind_id=$1 AND kind='do_not_infer'`, mind)
if err != nil {
return false, Correction{}
}
defer rows.Close()
pt, _ := Polarity(proposition)
for rows.Next() {
var c Correction
if err := rows.Scan(&c.ID, &c.Statement, &c.Reason); err != nil {
return false, Correction{}
}
ct, _ := Polarity(c.Statement)
if Jaccard(pt, ct) >= 0.7 {
return true, c
}
}
return false, Correction{}
}
// Versions returns the append-only history, newest first.
func (s *Store) Versions(ctx context.Context, mind, id ids.ID) ([]Version, error) {
rows, err := s.db.Query(ctx, `SELECT version, status, confidence, proposition, reason, breakdown, event_id, created_at FROM belief_versions WHERE belief_id=$1 AND mind_id=$2 ORDER BY version DESC`, id, mind)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Version
for rows.Next() {
var v Version
var st string
var bd []byte
var ev *string
if err := rows.Scan(&v.Version, &st, &v.Confidence, &v.Proposition, &v.Reason, &bd, &ev, &v.CreatedAt); err != nil {
return nil, err
}
v.Status = Status(st)
json.Unmarshal(bd, &v.Breakdown) //nolint:errcheck
if ev != nil {
v.EventID = ids.ID(*ev)
}
out = append(out, v)
}
return out, rows.Err()
}
// Alternatives lists competing explanations.
func (s *Store) Alternatives(ctx context.Context, mind, id ids.ID) ([]Alternative, error) {
rows, err := s.db.Query(ctx, `SELECT id, proposition, confidence, note, status, created_at FROM belief_alternatives WHERE belief_id=$1 AND mind_id=$2 ORDER BY created_at`, id, mind)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Alternative
for rows.Next() {
var a Alternative
if err := rows.Scan(&a.ID, &a.Proposition, &a.Confidence, &a.Note, &a.Status, &a.CreatedAt); err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}
// AddAlternative records another candidate explanation.
func (s *Store) AddAlternative(ctx context.Context, mind, id ids.ID, proposition, note string) (Alternative, error) {
proposition = strings.TrimSpace(proposition)
if proposition == "" {
return Alternative{}, fmt.Errorf("%w: proposition required", ErrInvalid)
}
a := Alternative{ID: ids.New(), Proposition: proposition, Confidence: 0.2, Note: note, Status: "open", CreatedAt: time.Now()}
tag, err := s.db.Exec(ctx, `INSERT INTO belief_alternatives (id, belief_id, mind_id, proposition, confidence, note) SELECT $1,$2,$3,$4,$5,$6 WHERE EXISTS (SELECT 1 FROM beliefs WHERE id=$2 AND mind_id=$3)`, a.ID, id, mind, a.Proposition, a.Confidence, a.Note)
if err != nil {
return Alternative{}, err
}
if tag.RowsAffected() == 0 {
return Alternative{}, storage.ErrNotFound
}
return a, nil
}
// Links returns contradiction and support links with the other belief loaded.
func (s *Store) Links(ctx context.Context, mind, id ids.ID) ([]Link, error) {
rows, err := s.db.Query(ctx, `SELECT l.id, l.from_id, l.to_id, l.relation, l.confidence, l.created_at, `+cols+` FROM belief_links l
JOIN beliefs b ON b.id = CASE WHEN l.from_id=$1 THEN l.to_id ELSE l.from_id END WHERE l.mind_id=$2 AND (l.from_id=$1 OR l.to_id=$1) ORDER BY l.created_at`, id, mind)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Link
for rows.Next() {
var l Link
var other Belief
var epi, status string
var detail []byte
var origin, superseded, pinned *string
if err := rows.Scan(&l.ID, &l.FromID, &l.ToID, &l.Relation, &l.Confidence, &l.CreatedAt,
&other.ID, &other.MindID, &other.Proposition, &other.Subject, &epi, &status, &other.Confidence, &other.Prior, &other.Version, &other.Source, &detail, &origin, &superseded, &pinned, &other.FirstCreated, &other.LastEvaluated, &other.Evaluations, &other.TemporalScope, &other.ObservedAt, &other.ValidFrom, &other.ValidUntil, &other.RedactedAt); err != nil {
return nil, err
}
other.Epistemic, other.Status = concepts.Epistemic(epi), Status(status)
l.Other = &other
out = append(out, l)
}
return out, rows.Err()
}
// RetractEvidence marks a piece of evidence as not counting (memory retracted etc.).
func (s *Store) RetractEvidence(ctx context.Context, q storage.Querier, mind ids.ID, kind, refID string) ([]ids.ID, error) {
rows, err := q.Query(ctx, `UPDATE belief_evidence SET retracted=true WHERE mind_id=$1 AND kind=$2 AND ref_id=$3 AND NOT retracted RETURNING belief_id`, mind, kind, refID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ids.ID
for rows.Next() {
var id ids.ID
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
return out, rows.Err()
}
// Stats summarises beliefs for the overview.
type Stats struct {
Total int64 `json:"total"`
ByStatus map[string]int64 `json:"by_status"`
}
// Stats counts beliefs by status.
func (s *Store) Stats(ctx context.Context, mind ids.ID) (Stats, error) {
st := Stats{ByStatus: map[string]int64{}}
rows, err := s.db.Query(ctx, `SELECT status, count(*) FROM beliefs WHERE mind_id=$1 GROUP BY status`, mind)
if err != nil {
return st, err
}
defer rows.Close()
for rows.Next() {
var k string
var n int64
if err := rows.Scan(&k, &n); err != nil {
return st, err
}
st.ByStatus[k] = n
st.Total += n
}
return st, rows.Err()
}
// Contested returns beliefs currently in conflict, for attention and reflection.
func (s *Store) Contested(ctx context.Context, mind ids.ID, limit int) ([]Belief, error) {
return s.List(ctx, mind, Filter{Statuses: []Status{StatusContested}, Limit: limit})
}
func trunc(s string, n int) string {
if len(s) > n {
return s[:n-1] + "…"
}
return s
}
// Search returns live beliefs matching any of the words in text, best match
// first. Used to answer "what do you believe about X".
func (s *Store) Search(ctx context.Context, mind ids.ID, text string, limit int) ([]Belief, error) {
toks, _ := Polarity(text)
if len(toks) == 0 {
return nil, nil
}
if limit <= 0 || limit > 50 {
limit = 10
}
q := strings.Join(toks, " | ")
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('superseded') AND b.search @@ to_tsquery('english', $2)
ORDER BY ts_rank_cd(b.search, to_tsquery('english', $2)) DESC, b.confidence DESC LIMIT $3`, mind, q, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Belief
for rows.Next() {
b, err := scan(rows)
if err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
// Change describes a belief whose status or confidence moved.
type Change struct {
Belief Belief `json:"belief"`
FromStatus Status `json:"from_status"`
FromConf float64 `json:"from_confidence"`
Reason string `json:"reason"`
At time.Time `json:"at"`
}
// RecentChanges lists beliefs whose latest version differs in status from an
// earlier version within the window ("what have you changed your mind about").
func (s *Store) RecentChanges(ctx context.Context, mind ids.ID, since time.Time, limit int) ([]Change, error) {
if limit <= 0 || limit > 50 {
limit = 10
}
rows, err := s.db.Query(ctx, `WITH latest AS (
SELECT DISTINCT ON (belief_id) belief_id, status, confidence, reason, created_at FROM belief_versions WHERE mind_id=$1 AND created_at >= $2 ORDER BY belief_id, version DESC),
first AS (
SELECT DISTINCT ON (belief_id) belief_id, status, confidence FROM belief_versions WHERE mind_id=$1 ORDER BY belief_id, version ASC)
SELECT `+cols+`, f.status, f.confidence, l.reason, l.created_at FROM latest l JOIN first f ON f.belief_id=l.belief_id JOIN beliefs b ON b.id=l.belief_id
WHERE l.status <> f.status ORDER BY l.created_at DESC LIMIT $3`, mind, since, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Change
for rows.Next() {
var c Change
var epi, status, fromStatus string
var detail []byte
var origin, superseded, pinned *string
b := &c.Belief
if err := rows.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil, &b.RedactedAt, &fromStatus, &c.FromConf, &c.Reason, &c.At); err != nil {
return nil, err
}
b.Epistemic, b.Status, c.FromStatus = concepts.Epistemic(epi), Status(status), Status(fromStatus)
out = append(out, c)
}
return out, rows.Err()
}
// Uncertain returns beliefs in the middle band or contested, least certain first.
func (s *Store) Uncertain(ctx context.Context, mind ids.ID, limit int) ([]Belief, error) {
if limit <= 0 || limit > 50 {
limit = 10
}
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('rejected','superseded') AND (b.status='contested' OR (b.confidence BETWEEN 0.3 AND 0.7))
ORDER BY abs(b.confidence-0.5) ASC, b.last_evaluated DESC LIMIT $2`, mind, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Belief
for rows.Next() {
b, err := scan(rows)
if err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
// closeState ends the validity of an older transient belief when a newer
// observation shows the state changed. Its score is untouched; its status
// becomes superseded with valid_until set, and a version and an event
// record why. "The door was open at 10:00" remains true.
func (s *Store) closeState(ctx context.Context, q storage.Querier, emit Emitter, older, newer Belief, sim float64, causation ids.ID) (events.Event, error) {
until := *newer.ObservedAt
ev, err := emit(ctx, q, events.New{MindID: older.MindID, Type: events.BeliefUpdated, Source: "beliefs.temporal", Summary: fmt.Sprintf("state changed: %q held until %s; now %q", trunc(older.Proposition, 60), until.Format("15:04:05"), trunc(newer.Proposition, 60)),
Salience: 0.5, Confidence: sim, CausationID: causation,
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(older.ID), Note: "closed"}, {Kind: "belief", ID: string(newer.ID), Note: "current"}},
Metadata: map[string]any{"belief_id": string(older.ID), "superseded_by": string(newer.ID), "valid_until": until, "reason": "state changed"},
Payload: map[string]any{"older": map[string]any{"id": older.ID, "proposition": older.Proposition, "valid_from": older.ValidFrom, "valid_until": until}, "newer": map[string]any{"id": newer.ID, "proposition": newer.Proposition, "observed_at": newer.ObservedAt}, "method": "temporal supersession"}})
if err != nil {
return events.Event{}, err
}
if _, err := q.Exec(ctx, `UPDATE beliefs SET status='superseded', superseded_by=$3, valid_until=$4, version=version+1, last_evaluated=now(), evaluations=evaluations+1 WHERE id=$1 AND mind_id=$2`, older.ID, older.MindID, newer.ID, until); err != nil {
return events.Event{}, err
}
older.Version++
older.Status = StatusSuperseded
if err := s.insertVersion(ctx, q, older, Breakdown{Prior: older.Prior, Confidence: older.Confidence, Meaning: ScoreMeaning, Notes: []string{"state changed; score unchanged, validity closed"}}, "state changed: superseded by a later observation", ev.ID); err != nil {
return events.Event{}, err
}
if _, err := q.Exec(ctx, `INSERT INTO belief_links (id, mind_id, from_id, to_id, relation, confidence, event_id) VALUES ($1,$2,$3,$4,'supersedes',$5,$6) ON CONFLICT DO NOTHING`, ids.New(), older.MindID, newer.ID, older.ID, sim, ev.ID); err != nil {
return events.Event{}, err
}
return ev, nil
}
// History lists superseded beliefs about a topic with their validity, so a
// mind can say what was true and when.
func (s *Store) History(ctx context.Context, mind ids.ID, text string, limit int) ([]Belief, error) {
if limit <= 0 || limit > 50 {
limit = 10
}
toks, _ := Polarity(text)
if len(toks) == 0 {
return nil, nil
}
q := strings.Join(toks, " | ")
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status='superseded' AND b.valid_until IS NOT NULL AND b.search @@ to_tsquery('english', $2) ORDER BY b.valid_until DESC LIMIT $3`, mind, q, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Belief
for rows.Next() {
b, err := scan(rows)
if err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
// VersionAt is a belief as it stood at a moment, from the append-only
// version history.
type VersionAt struct {
BeliefID ids.ID `json:"belief_id"`
Proposition string `json:"proposition"`
Status string `json:"status"`
Confidence float64 `json:"confidence"`
Version int `json:"version"`
RecordedAt time.Time `json:"recorded_at"`
}
// AsOf returns what the mind held at a moment: for each belief matching the
// text (all beliefs when text has no usable terms), the latest version
// recorded at or before at. Beliefs created later are absent by definition.
func (s *Store) AsOf(ctx context.Context, mind ids.ID, text string, at time.Time, limit int) ([]VersionAt, error) {
if limit <= 0 || limit > 50 {
limit = 10
}
toks, _ := Polarity(text)
q := strings.Join(toks, " | ")
rows, err := s.db.Query(ctx, `SELECT DISTINCT ON (v.belief_id) v.belief_id, v.proposition, v.status, v.confidence, v.version, v.created_at
FROM belief_versions v JOIN beliefs b ON b.id=v.belief_id
WHERE v.mind_id=$1 AND v.created_at <= $2 AND ($3='' OR b.search @@ to_tsquery('english', $3))
ORDER BY v.belief_id, v.version DESC`, mind, at, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []VersionAt
for rows.Next() {
var v VersionAt
if err := rows.Scan(&v.BeliefID, &v.Proposition, &v.Status, &v.Confidence, &v.Version, &v.RecordedAt); err != nil {
return nil, err
}
out = append(out, v)
}
sortVersions(out)
if len(out) > limit {
out = out[:limit]
}
return out, rows.Err()
}
func sortVersions(vs []VersionAt) {
for i := 1; i < len(vs); i++ {
for j := i; j > 0 && vs[j].Confidence > vs[j-1].Confidence; j-- {
vs[j], vs[j-1] = vs[j-1], vs[j]
}
}
}