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
+39
View File
@@ -1,6 +1,7 @@
package cognition
import (
"encoding/json"
"context"
"sort"
"strings"
@@ -8,6 +9,7 @@ import (
"github.com/andydixon/noema/internal/attention"
"github.com/andydixon/noema/internal/brain"
"github.com/andydixon/noema/internal/epistemics"
"github.com/andydixon/noema/internal/workspace"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
@@ -43,10 +45,29 @@ func (a Associate) Observe(_ context.Context, s *brain.State) ([]brain.Candidate
if laterU < 0.4 {
continue
}
// Speech is not a world event. A hypothetical or a question cannot be a
// cause or an effect, and two turns by the same speaker in one
// conversation are one person talking, not a sequence of happenings.
speaker := ""
if ev.Metadata["conversation_id"] != nil {
var p events.ObservationPayload
if json.Unmarshal(ev.Payload, &p) == nil {
speaker = p.Actor
}
if !worldly(speaker, ev.Summary) {
continue
}
}
for _, it := range s.Workspace.WorkingMemory {
if it.Kind != "observation" || it.SourceEvent.IsZero() || it.SourceEvent == ev.ID {
continue
}
if speaker != "" && strings.HasPrefix(it.Content, speaker+": ") {
continue
}
if !worldly("", it.Content) {
continue
}
if ev.OccurredAt.Sub(it.TouchedAt) > window || it.TouchedAt.After(ev.OccurredAt) {
continue
}
@@ -74,3 +95,21 @@ func toSet(ws []string) map[string]struct{} {
}
return m
}
// worldly reports whether an utterance describes something that happened,
// as opposed to a hypothetical, a question or a request, which cannot stand
// as a cause or an effect.
func worldly(speaker, summary string) bool {
text := summary
if speaker != "" {
text = strings.TrimPrefix(text, speaker+": ")
} else if i := strings.Index(text, ": "); i > 0 && i < 40 {
text = text[i+2:]
}
in := epistemics.Interpret(speaker, text)
switch in.Modality {
case epistemics.ModalityHypothetical, epistemics.ModalityQuestion, epistemics.ModalityImperative:
return false
}
return true
}