Every evidence row now records its derivation (direct, assertion, operator, external, prediction, consolidation, inference, reflection, dream, retrieval), its independent-source family (an actor, an integration, an operator, a prediction) and its root event or memory ids. Confidence groups evidence by family: the strongest primary item counts in full, each further primary item adds a quarter, capped at 1.5x, and derived items count only when the family has no primary member. Three derived re-expressions of one utterance therefore weigh exactly what the utterance weighs, while three independent families still add up. Producers (conversation, association, consolidation, predictions, metacognition, dreaming, operator and seed input, contradictions) supply their ancestry. Existing rows are marked legacy_unknown with family = source; nothing is invented. The belief page shows per-family contributions and the replay test checks that provenance is reconstructed identically in the sandbox. Migration 0017 is additive.
105 lines
4.5 KiB
Go
105 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/andydixon/noema/internal/auth"
|
|
"github.com/andydixon/noema/internal/beliefs"
|
|
"github.com/andydixon/noema/internal/memory"
|
|
"github.com/andydixon/noema/internal/minds"
|
|
"github.com/andydixon/noema/pkg/concepts"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
)
|
|
|
|
// Seed is the mind-seed document (schema noema.mind-seed/1): identity,
|
|
// personality, configuration and starting knowledge. It is not the full
|
|
// export/import archive (phase 15); it is how sample and template minds are
|
|
// loaded. Treated as hostile input: strict decoding, size limits, operator
|
|
// trust on every item, no secrets, no policies.
|
|
type Seed struct {
|
|
Schema string `json:"schema"`
|
|
Slug string `json:"slug"`
|
|
DisplayName string `json:"display_name"`
|
|
Notes string `json:"notes"`
|
|
Personality *minds.Personality `json:"personality"`
|
|
Config *minds.CognitiveConfig `json:"config"`
|
|
Memories []struct {
|
|
Kind concepts.MemoryKind `json:"kind"`
|
|
Epistemic concepts.Epistemic `json:"epistemic"`
|
|
Content string `json:"content"`
|
|
Confidence float64 `json:"confidence"`
|
|
Importance float64 `json:"importance"`
|
|
} `json:"memories"`
|
|
Beliefs []struct {
|
|
Proposition string `json:"proposition"`
|
|
Subject string `json:"subject"`
|
|
Epistemic concepts.Epistemic `json:"epistemic"`
|
|
Weight float64 `json:"weight"`
|
|
Note string `json:"note"`
|
|
} `json:"beliefs"`
|
|
}
|
|
|
|
const maxSeedItems = 500
|
|
|
|
func (s *Server) importRoutes() {
|
|
s.mux.Handle("POST /api/v1/minds/import", require(auth.RoleAdministrator, s.mindImport))
|
|
}
|
|
|
|
// mindImport creates a mind from a seed document.
|
|
func (s *Server) mindImport(w http.ResponseWriter, r *http.Request) {
|
|
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20))
|
|
dec.DisallowUnknownFields()
|
|
var seed Seed
|
|
if err := dec.Decode(&seed); err != nil {
|
|
fail(w, r, ValidationError("invalid seed: "+err.Error()))
|
|
return
|
|
}
|
|
if seed.Schema != "noema.mind-seed/1" {
|
|
fail(w, r, ValidationError("unsupported schema; expected noema.mind-seed/1"))
|
|
return
|
|
}
|
|
if len(seed.Memories) > maxSeedItems || len(seed.Beliefs) > maxSeedItems {
|
|
fail(w, r, ValidationError("too many seed items"))
|
|
return
|
|
}
|
|
m, err := s.deps.Minds.Create(r.Context(), minds.CreateParams{Slug: seed.Slug, DisplayName: seed.DisplayName, Personality: seed.Personality, Config: seed.Config, Notes: seed.Notes, CreatedBy: actor(r)})
|
|
if err != nil {
|
|
fail(w, r, mindErr(err))
|
|
return
|
|
}
|
|
var memCount, beliefCount int
|
|
var problems []string
|
|
for _, sm := range seed.Memories {
|
|
mem, err := s.deps.Memory.Create(r.Context(), s.deps.DB, memory.New{MindID: m.ID, Kind: sm.Kind, Epistemic: sm.Epistemic, Content: sm.Content, Source: "seed", Trust: events.TrustOperator, Confidence: sm.Confidence, Importance: sm.Importance, Salience: 0.5})
|
|
if err != nil {
|
|
problems = append(problems, "memory: "+err.Error())
|
|
continue
|
|
}
|
|
memCount++
|
|
s.deps.Events.Append(r.Context(), events.New{MindID: m.ID, Type: events.MemoryCreated, Source: "seed", Summary: "seeded " + string(mem.Kind) + " memory: " + trunc(mem.Content, 120), Salience: 0.3, Confidence: mem.Confidence, //nolint:errcheck
|
|
Evidence: []events.EvidenceRef{{Kind: "memory", ID: string(mem.ID)}}, Metadata: map[string]any{"memory_id": mem.ID, "kind": mem.Kind, "actor": actor(r)}})
|
|
}
|
|
for _, sb := range seed.Beliefs {
|
|
weight := sb.Weight
|
|
if weight <= 0 {
|
|
weight = 0.5
|
|
}
|
|
_, err := s.deps.Beliefs.Create(r.Context(), s.deps.DB, s.Emitter(), beliefs.New{MindID: m.ID, Proposition: sb.Proposition, Subject: sb.Subject, Epistemic: sb.Epistemic, Source: "seed",
|
|
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "seed:" + actor(r), Weight: weight, Credibility: events.TrustOperator.Credibility(), Note: firstNonEmpty(sb.Note, "seeded by "+actor(r)), Derivation: beliefs.DerivationOperator, Family: "seed:" + actor(r)}}})
|
|
if err != nil {
|
|
if errors.Is(err, beliefs.ErrInvalid) || errors.Is(err, beliefs.ErrBlocked) {
|
|
problems = append(problems, "belief: "+err.Error())
|
|
continue
|
|
}
|
|
fail(w, r, err)
|
|
return
|
|
}
|
|
beliefCount++
|
|
}
|
|
s.deps.Brain.Reconcile(r.Context()) //nolint:errcheck
|
|
s.deps.Audit.Log(r.Context(), "mind.import", "mind", m.ID.String(), map[string]any{"slug": m.Slug, "memories": memCount, "beliefs": beliefCount, "problems": problems})
|
|
writeJSON(w, http.StatusCreated, map[string]any{"mind": m, "memories": memCount, "beliefs": beliefCount, "problems": problems})
|
|
}
|