feat(epistemics): record assertions between observations and beliefs

A statement made to a mind is now three things kept apart: the observation
that it arrived, an append-only assertion (attributed actor, stance,
certainty, modality, negation, reported or quoted speech, hypothetical
status, interpretation confidence and notes), and, only for factual
assertions, one piece of evidence for a proposition belief attributed to
whoever actually stands behind it. Reported speech is attributed to the
person reported and weighed as second-hand; hypotheticals, questions and
imperatives create no belief; expressed doubt counts against; a possibly
sarcastic remark is recorded with low interpretation confidence rather
than guessed at. Interpretation is deterministic code; a model may enrich
the claim list but never sets stance or confidence.

Association no longer treats conversational turns as world events:
hypotheticals, questions, requests and consecutive turns by one speaker
cannot seed causal hypotheses.

Adds internal/epistemics, migration 0018, the assertions API, assertions
on belief pages and in belief detail, a shared end-to-end test harness
(tests/harness), end-to-end tests for eight speech patterns, and updates
the milestone test to the new provenance.
This commit is contained in:
Andy Dixon
2026-08-05 19:22:48 +01:00
parent 0071658c93
commit 9b2521bffd
28 changed files with 1447 additions and 325 deletions
+16 -8
View File
@@ -9,6 +9,7 @@ import (
"github.com/andydixon/noema/internal/auth"
"github.com/andydixon/noema/internal/beliefs"
"github.com/andydixon/noema/internal/epistemics"
"github.com/andydixon/noema/internal/storage"
"github.com/andydixon/noema/pkg/concepts"
"github.com/andydixon/noema/pkg/events"
@@ -129,14 +130,15 @@ func firstNonEmpty(a, b string) string {
// BeliefDetail is the full picture of one belief.
type BeliefDetail struct {
Belief beliefs.Belief `json:"belief"`
Breakdown beliefs.Breakdown `json:"breakdown"`
Evidence []beliefs.EvidenceRow `json:"evidence"`
Versions []beliefs.Version `json:"versions"`
Alternatives []beliefs.Alternative `json:"alternatives"`
Links []beliefs.Link `json:"links"`
Events []events.Event `json:"events"`
Origin *events.Event `json:"origin,omitempty"`
Belief beliefs.Belief `json:"belief"`
Breakdown beliefs.Breakdown `json:"breakdown"`
Evidence []beliefs.EvidenceRow `json:"evidence"`
Versions []beliefs.Version `json:"versions"`
Alternatives []beliefs.Alternative `json:"alternatives"`
Links []beliefs.Link `json:"links"`
Events []events.Event `json:"events"`
Origin *events.Event `json:"origin,omitempty"`
Assertions []epistemics.Assertion `json:"assertions"` // who stated this proposition, with what stance
}
// BeliefDetail assembles the detail; shared with the UI.
@@ -156,6 +158,12 @@ func (s *Server) BeliefDetail(r *http.Request, mind, id ids.ID) (BeliefDetail, e
d.Versions, _ = s.deps.Beliefs.Versions(ctx, mind, id)
d.Alternatives, _ = s.deps.Beliefs.Alternatives(ctx, mind, id)
d.Links, _ = s.deps.Beliefs.Links(ctx, mind, id)
if s.deps.Assertions != nil {
d.Assertions, _ = s.deps.Assertions.List(ctx, mind, epistemics.Filter{BeliefID: id, Limit: 50})
}
if d.Assertions == nil {
d.Assertions = []epistemics.Assertion{}
}
d.Events, _ = s.deps.Events.DerivedFrom(ctx, "belief", string(id), 100)
if !b.OriginEvent.IsZero() {
if e, err := s.deps.Events.Get(ctx, b.OriginEvent); err == nil {