Files
Andy Dixon 9b2521bffd 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.
2026-08-05 19:22:48 +01:00

70 lines
1.7 KiB
Go

package api
import (
"net/http"
"strconv"
"github.com/andydixon/noema/internal/auth"
"github.com/andydixon/noema/internal/epistemics"
"github.com/andydixon/noema/pkg/ids"
)
// assertionRoutes expose who asserted what to a mind, kept apart from what
// the mind believes.
func (s *Server) assertionRoutes() {
m := s.mux
m.Handle("GET /api/v1/minds/{mind}/assertions", require(auth.RoleObserver, s.assertionsList))
m.Handle("GET /api/v1/minds/{mind}/assertions/actors", require(auth.RoleObserver, s.assertionActors))
}
func (s *Server) assertionsList(w http.ResponseWriter, r *http.Request) {
m, ok := s.mind(w, r)
if !ok {
return
}
q := r.URL.Query()
f := epistemics.Filter{AttributedTo: q.Get("actor"), Stance: q.Get("stance"), Limit: intParam(q.Get("limit"), 100)}
if b := q.Get("belief"); b != "" {
id, err := ids.Parse(b)
if err != nil {
fail(w, r, ValidationError("bad belief id"))
return
}
f.BeliefID = id
}
list, err := s.deps.Assertions.List(r.Context(), m.ID, f)
if err != nil {
fail(w, r, err)
return
}
if list == nil {
list = []epistemics.Assertion{}
}
writeJSON(w, http.StatusOK, map[string]any{"items": list})
}
func (s *Server) assertionActors(w http.ResponseWriter, r *http.Request) {
m, ok := s.mind(w, r)
if !ok {
return
}
list, err := s.deps.Assertions.Actors(r.Context(), m.ID)
if err != nil {
fail(w, r, err)
return
}
if list == nil {
list = []epistemics.ActorSummary{}
}
writeJSON(w, http.StatusOK, map[string]any{"items": list})
}
// intParam parses a bounded positive integer query value.
func intParam(v string, def int) int {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
return def
}
return n
}