feat(beliefs): temporal scope and validity; state changes supersede rather than contradict

Beliefs gain a temporal scope (persistent, transient or unknown, classified
deterministically from the proposition), an observation time and, for
transient states, a validity interval. When a newer belief conflicts with
an older transient one observed at least a minute earlier, the older belief
is closed (valid_until set, status superseded, a supersedes link, a version
and an event saying 'state changed') with its score untouched: 'the door
was open at 10:00' stays true of 10:00 when it is closed at 10:05.
Simultaneous conflicting reports and persistent propositions contradict as
before. Introspection lists superseded states with their validity.
Migration 0020 is additive.
This commit is contained in:
Andy Dixon
2026-08-09 21:42:19 +01:00
parent b661496128
commit 7821d89cf5
9 changed files with 256 additions and 8 deletions
+1
View File
@@ -10,6 +10,7 @@ All notable changes to Noema are recorded here. Format follows Keep a Changelog;
- 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.
### Added
- Temporal beliefs: every belief has a temporal scope (persistent, transient, unknown, classified deterministically), an observation time and, for transient states, a validity interval. A later observation that a transient state changed closes the earlier belief (valid_until, superseded, score unchanged) instead of contesting it; simultaneous conflicts and persistent propositions still contradict. Introspection reports what was true and when. Migration 0020.
- Confidence semantics: every breakdown states that the number is an uncalibrated epistemic score; belief detail adds a calibrated estimate from the mind's resolved predictions when at least twenty were decided in the same range (otherwise says why not) and names the primary uncertainty deterministically. `docs/beliefs.md` defines every epistemic number.
- Explicit `unknown` belief status: no live evidence either way is neither tentative belief nor falsity. Migration 0019.
- Assertions: every statement made to a mind is recorded first as an append-only assertion (attributed actor, stance, certainty, modality, negation, reported or quoted speech, hypothetical status, interpretation confidence and notes) before, and separately from, any belief about the proposition. Reported speech is attributed to the person reported and weighed as second-hand; hypotheticals, questions and imperatives create no belief; expressed doubt counts against. API `GET /api/v1/minds/{mind}/assertions`, belief pages show who said so. Migration 0018.
+6
View File
@@ -72,6 +72,12 @@ Derived from the breakdown unless an operator pinned it:
- `rejected` below 0.15; `hypothesis` for hypotheses without support or below 0.5; `tentative` below 0.7; `likely` below 0.85; `accepted` above;
- `superseded` when replaced by a newer proposition.
## Temporal scope
Every belief has a `temporal_scope`: `persistent` (a fact without an interval: "atlas is the primary host", "Andy prefers Go"), `transient` (a state that can change: "the door is open", "server A is down") or `unknown` when the deterministic classifier (`ClassifyScope`) cannot tell. Transient beliefs record `observed_at` and `valid_from`.
When a newer belief conflicts with an older **transient** one and was observed at least a minute later, the older belief is not contested and not weakened: its `valid_until` is set to the newer observation, its status becomes `superseded` with a version and a `belief_updated` event saying "state changed", and a `supersedes` link joins them. "The door was open at 10:00" stays true of 10:00 when the door is closed at 10:05. Conflicting reports of the same moment, and conflicts between persistent propositions, are contradictions as described below. Introspection lists superseded states as "Earlier: … (true from … until …)".
## Contradictions
On creation and on gaining support, a belief is compared with the mind's live beliefs. The deterministic pass detects the same content with flipped polarity (negation words plus token Jaccard ≥ 0.6). A hit emits `contradiction_detected`, links both beliefs, and makes each counter-evidence for the other weighted by the other's confidence, so both move towards `contested` until more evidence arrives. Paraphrase-level judgement is added by an LLM-assisted process once providers exist; it can only propose, and the same arithmetic applies.
+95 -7
View File
@@ -36,6 +36,13 @@ type Belief struct {
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"`
}
// EvidenceRow is stored evidence.
@@ -125,6 +132,10 @@ type New struct {
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.
@@ -170,14 +181,26 @@ func (s *Store) Create(ctx context.Context, q storage.Querier, emit Emitter, n N
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}
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)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$10,$11)`, b.ID, b.MindID, b.Proposition, b.Subject, string(b.Epistemic), string(b.Status), b.Confidence, b.Prior, b.Source, detail, nullID(b.OriginEvent)); err != nil {
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
@@ -274,14 +297,14 @@ func nullID(id ids.ID) *string {
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`
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`
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); err != nil {
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); err != nil {
return b, storage.Translate(err)
}
b.Epistemic, b.Status = concepts.Epistemic(epi), Status(status)
@@ -528,6 +551,16 @@ func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emi
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)}},
@@ -750,7 +783,7 @@ func (s *Store) Links(ctx context.Context, mind, id ids.ID) ([]Link, error) {
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); err != nil {
&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); err != nil {
return nil, err
}
other.Epistemic, other.Status = concepts.Epistemic(epi), Status(status)
@@ -876,7 +909,7 @@ func (s *Store) RecentChanges(ctx context.Context, mind ids.ID, since time.Time,
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, &fromStatus, &c.FromConf, &c.Reason, &c.At); err != nil {
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, &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)
@@ -906,3 +939,58 @@ func (s *Store) Uncertain(ctx context.Context, mind ids.ID, limit int) ([]Belief
}
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()
}
+68
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/minds"
@@ -161,3 +162,70 @@ func TestUnknownIsNotFalse(t *testing.T) {
t.Fatal("absence of evidence must not read as falsity")
}
}
// TestTransientStatesSupersedeInsteadOfContesting: a later observation of a
// changed state closes the earlier belief's validity and leaves it true of
// its time; persistent propositions still contradict.
func TestTransientStatesSupersedeInsteadOfContesting(t *testing.T) {
if ClassifyScope("the door is open") != ScopeTransient || ClassifyScope("server-a is down") != ScopeTransient || ClassifyScope("atlas is the primary database host") != ScopePersistent || ClassifyScope("andy prefers go") != ScopePersistent {
t.Fatal("scope classifier")
}
pool := storage.TestPool(t, migrations.FS)
ctx := context.Background()
m, _ := minds.NewStore(pool).Create(ctx, minds.CreateParams{Slug: "tmp"})
es := evstore.NewStore(pool, evstore.NewBus(8), nil)
emit := func(ctx context.Context, q storage.Querier, n events.New) (events.Event, error) {
out, err := es.AppendIn(ctx, q, n)
if err != nil {
return events.Event{}, err
}
return out[0], nil
}
s := NewStore(pool)
ten := time.Date(2026, 9, 15, 10, 0, 0, 0, time.UTC)
tenFive := ten.Add(5 * time.Minute)
ev := []EvidenceInput{{Direction: "for", Kind: "note", Source: "sensor", Weight: 0.7, Credibility: 0.9}}
open, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the door is open", Subject: "door", Epistemic: concepts.Observation, Source: "sensor", Evidence: ev, ObservedAt: &ten})
if err != nil || open.Belief.TemporalScope != ScopeTransient || open.Belief.ValidFrom == nil {
t.Fatalf("open: %+v %v", open.Belief, err)
}
closed, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the door is not open", Subject: "door", Epistemic: concepts.Observation, Source: "sensor", Evidence: ev, ObservedAt: &tenFive})
if err != nil {
t.Fatal(err)
}
before, _ := s.Get(ctx, pool, m.ID, open.Belief.ID)
if before.Status != StatusSuperseded || before.ValidUntil == nil || !before.ValidUntil.Equal(tenFive) || before.SupersededBy != closed.Belief.ID {
t.Fatalf("earlier state should be closed, not contested: %+v", before)
}
if before.Confidence != open.Belief.Confidence {
t.Fatalf("closing a state must not change its score: %.3f -> %.3f", open.Belief.Confidence, before.Confidence)
}
now, _ := s.Get(ctx, pool, m.ID, closed.Belief.ID)
if now.Status == StatusContested || now.Confidence < closed.Belief.Confidence {
t.Fatalf("current state should not be contested: %+v", now)
}
if rows, _ := s.EvidenceFor(ctx, pool, m.ID, now.ID); len(rows) != 1 {
t.Fatalf("no counter-evidence should be written for a state change: %+v", rows)
}
hist, _ := s.History(ctx, m.ID, "door open", 5)
if len(hist) != 1 || hist[0].ID != open.Belief.ID {
t.Fatalf("history: %+v", hist)
}
// Same-moment conflicting reports remain a contradiction.
a, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the pump is running", Subject: "pump", Epistemic: concepts.Observation, Source: "a", Evidence: ev, ObservedAt: &ten})
b, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the pump is not running", Subject: "pump", Epistemic: concepts.Observation, Source: "b", Evidence: ev, ObservedAt: &ten})
ab, _ := s.Get(ctx, pool, m.ID, a.Belief.ID)
if ab.Status == StatusSuperseded || ab.ValidUntil != nil {
t.Fatalf("simultaneous conflict must contest, not supersede: %+v", ab)
}
if links, _ := s.Links(ctx, m.ID, b.Belief.ID); len(links) != 1 || links[0].Relation != "contradicts" {
t.Fatalf("contradiction link expected: %+v", links)
}
// Persistent propositions contradict regardless of time.
p1, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "atlas is the primary database host", Subject: "atlas", Epistemic: concepts.Claim, Source: "x", Evidence: ev, ObservedAt: &ten})
s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "atlas is not the primary database host", Subject: "atlas", Epistemic: concepts.Claim, Source: "y", Evidence: ev, ObservedAt: &tenFive}) //nolint:errcheck
p1b, _ := s.Get(ctx, pool, m.ID, p1.Belief.ID)
if p1b.Status == StatusSuperseded {
t.Fatalf("persistent fact wrongly treated as a state change: %+v", p1b)
}
}
+55
View File
@@ -0,0 +1,55 @@
package beliefs
import (
"regexp"
"strings"
"time"
)
// Temporal scopes. Persistent propositions are true or false without an
// interval ("atlas is the primary host", "Andy prefers Go"). Transient ones
// describe a state that can change ("the door is open", "server A is down")
// and carry a validity interval. Unknown is the honest default when the
// classifier cannot tell.
const (
ScopePersistent = "persistent"
ScopeTransient = "transient"
ScopeUnknown = "unknown"
)
// MinStateChangeGap is the shortest interval between two observations for a
// later, conflicting observation of a transient state to count as a state
// change rather than a contradiction. Two reports of the same moment that
// disagree are a contradiction; reports minutes apart are a change.
const MinStateChangeGap = time.Minute
var (
reTransient = regexp.MustCompile(`(?i)\b(?:is|are|was|were|remains|seems|looks|appears|has been|have been|is currently|is now)\s+(?:not\s+|still\s+|no longer\s+)?(?:down|up|open|closed|offline|online|running|stopped|stuck|failing|degraded|full|empty|busy|idle|locked|unlocked|available|unavailable|slow|broken|healthy|unhealthy|overloaded|unreachable|reachable|responding|paused|active|inactive|pending|in progress|at capacity|on call|away|present|absent|late|early|hot|cold|warm|crossed)\b|\b(?:right now|currently|at the moment|at \d{1,2}:\d{2}|between \d|since \d|until \d|today|tonight|this morning|this afternoon|for the last|in the last)\b`)
rePersistent = regexp.MustCompile(`(?i)\b(?:is (?:a|an|the)\b|are (?:a|an|the)\b|was born|prefers?|likes?|dislikes?|always|never|is named|is called|is the name|belongs to|owns|is made of|is located in|is based in|works at|works for|is part of|means|stands for|is written in|uses|runs on|depends on)\b`)
)
// ClassifyScope decides, deterministically, whether a proposition names a
// transient state, a persistent fact, or neither recognisably.
func ClassifyScope(proposition string) string {
p := strings.TrimSpace(proposition)
switch {
case reTransient.MatchString(p):
return ScopeTransient
case rePersistent.MatchString(p):
return ScopePersistent
}
return ScopeUnknown
}
// StateChange reports whether a newer belief should close an older,
// conflicting one as a change of state rather than contest it: both must be
// transient, and the newer observation must be later by at least the gap.
func StateChange(older, newer Belief) bool {
if older.TemporalScope != ScopeTransient || newer.TemporalScope != ScopeTransient {
return false
}
if older.ObservedAt == nil || newer.ObservedAt == nil {
return false
}
return newer.ObservedAt.Sub(*older.ObservedAt) >= MinStateChangeGap
}
+13 -1
View File
@@ -123,7 +123,8 @@ func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thou
Note: actor + " doubts it", Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}})
}
var err error
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev,
observed := c.State.Now
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev, ObservedAt: &observed,
Detail: map[string]any{"attributed_to": attributed, "reported": in.Reported, "interpretation_confidence": in.Confidence}})
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"}})
@@ -304,6 +305,17 @@ func (h Handlers) addKnowledge(ctx context.Context, c *brain.Cycle, in *Intent,
in.Uncertainty = fmt.Sprintf("The strongest relevant belief is only at %.2f.", maxConf)
}
}
// Historical truth is not overwritten by the present: say what was
// true and when, alongside what is believed now.
if past, _ := h.Beliefs.History(ctx, mind, topic, 3); len(past) > 0 {
for _, b := range past {
from := "earlier"
if b.ValidFrom != nil {
from = b.ValidFrom.Format("15:04 on 2 Jan")
}
in.Points = append(in.Points, fmt.Sprintf("Earlier: %s (true from %s until %s, then superseded).", b.Proposition, from, b.ValidUntil.Format("15:04 on 2 Jan")))
}
}
}
if h.Memory != nil {
if hits, _ := h.Memory.Retrieve(ctx, mind, memory.Query{Text: topic, Limit: 4, Now: c.State.Now, Semantic: true}); len(hits) > 0 {
@@ -0,0 +1,6 @@
DROP INDEX IF EXISTS beliefs_mind_subject_validity_idx;
ALTER TABLE beliefs
DROP COLUMN IF EXISTS valid_until,
DROP COLUMN IF EXISTS valid_from,
DROP COLUMN IF EXISTS observed_at,
DROP COLUMN IF EXISTS temporal_scope;
+11
View File
@@ -0,0 +1,11 @@
-- Temporal scope (review pass §7.2 item 12). A belief may describe a
-- persistent fact or a transient state. Transient states have a validity
-- interval: a later observation that the state changed closes the interval
-- and supersedes the belief instead of contesting it, so what was true at
-- 10:00 stays true of 10:00 when 10:05 differs.
ALTER TABLE beliefs
ADD COLUMN IF NOT EXISTS temporal_scope text NOT NULL DEFAULT 'unknown' CHECK (temporal_scope IN ('persistent','transient','unknown')),
ADD COLUMN IF NOT EXISTS observed_at timestamptz,
ADD COLUMN IF NOT EXISTS valid_from timestamptz,
ADD COLUMN IF NOT EXISTS valid_until timestamptz;
CREATE INDEX IF NOT EXISTS beliefs_mind_subject_validity_idx ON beliefs (mind_id, subject, valid_until);
+1
View File
@@ -6,6 +6,7 @@
<p class="score-line"><strong>Epistemic score {{conf $b.Confidence}}</strong> <span class="muted small">(uncalibrated: derived from recorded evidence, not a probability of truth)</span>
· calibrated: {{if $d.Calibration.Available}}<strong>{{conf $d.Calibration.Estimate}}</strong> <span class="muted small">(hit rate of {{$d.Calibration.SampleSize}} decided predictions in this range)</span>{{else}}<span class="muted small">not available ({{$d.Calibration.Reason}})</span>{{end}}
· main uncertainty: <span class="muted">{{$d.PrimaryUncertainty}}</span></p>
<p class="muted small">temporal scope <span class="badge small">{{$b.TemporalScope}}</span>{{with $b.ObservedAt}} · observed {{date .}}{{end}}{{with $b.ValidFrom}} · valid from {{date .}}{{end}}{{with $b.ValidUntil}} · <strong>until {{date .}}</strong> (superseded by a later observation; this remained true of its time){{end}}</p>
<p class="muted">{{$b.Epistemic}} · version {{$b.Version}} · first {{date $b.FirstCreated}} · evaluated {{$b.Evaluations}}× · source {{$b.Source}}{{if $b.Subject}} · subject <span class="mono">{{$b.Subject}}</span>{{end}}{{if $b.PinnedStatus}} · <span class="badge warn">status pinned by operator</span>{{end}}</p>
</div>
<section class="grid cols-3-1">