diff --git a/docs/graph.md b/docs/graph.md index 47cecda..dc49e45 100644 --- a/docs/graph.md +++ b/docs/graph.md @@ -10,7 +10,17 @@ Extraction is deterministic and conservative: hostnames (`atlas.local`), hyphena ## Relationships -Edges carry a relation from the shared vocabulary (`related_to`, `discussed_with`, `caused_by`, `supports`, `contradicts`, `depends_on`, `belongs_to`, `observed_during`, `derived_from`, `similar_to`, `follows`), a confidence, a source, a count of reinforcements and the event that produced them. Co-mention in one message yields `discussed_with` (speaker → entity) and `related_to` (entity ↔ entity) at low confidence; repeats raise the count. Operators can add typed edges with higher confidence. +Edges carry a relation from the shared vocabulary (`co_occurs_with`, `related_to`, `discussed_with`, `temporally_precedes`, `possibly_caused_by`, `caused_by`, `supports`, `contradicts`, `depends_on`, `belongs_to`, `observed_during`, `derived_from`, `similar_to`, `follows`, `asserted_by`), a confidence, a source, a count of reinforcements and the event that produced them. Co-mention in one message yields `discussed_with` (speaker → entity) and `co_occurs_with` (entity ↔ entity) at low confidence; repeats raise the count but never change the kind. Operators can add typed edges with higher confidence. + +## Association is not causation + +Repeated association is evidence of association, nothing more. The vocabulary keeps the distinction explicit: + +- `co_occurs_with` and `temporally_precedes` record what was observed together or in order; +- `possibly_caused_by` is a **hypothesis**: the association process proposes it for notable events close in time, always as a `hypothesis` belief with the alternatives "coincidence" and "a third factor caused both", and a prediction that the effect should recur after the cause; +- `caused_by` is reserved for established causes: an explicit operator statement or a correction. Nothing automatic writes it. + +Speech acts are not world events: questions, hypotheticals, requests and consecutive turns by one speaker never seed a causal hypothesis. `entity_links` record which memories, beliefs, events, goals and predictions involve an entity, so the entity page answers "where has this come up". diff --git a/internal/cognition/entities.go b/internal/cognition/entities.go index e12024d..4c09821 100644 --- a/internal/cognition/entities.go +++ b/internal/cognition/entities.go @@ -45,10 +45,12 @@ func (h Handlers) linkEntities(ctx context.Context, c *brain.Cycle, names []stri add("", n) } } - // The speaker discussed each entity; mentioned entities relate to each other. + // The speaker discussed each entity; entities mentioned together merely + // co-occur. Co-occurrence is association, never a causal or structural + // claim; stronger relations need an explicit statement or an operator. for i := range ents { for j := i + 1; j < len(ents); j++ { - rel := concepts.RelatedTo + rel := concepts.CoOccursWith if ents[i].Kind == graph.Person && i == 0 && actor != "" { rel = concepts.DiscussedWith } diff --git a/internal/cognition/handlers.go b/internal/cognition/handlers.go index 9843750..3624566 100644 --- a/internal/cognition/handlers.go +++ b/internal/cognition/handlers.go @@ -84,7 +84,7 @@ func (h Handlers) hypothesis(ctx context.Context, c *brain.Cycle, t workspace.Th } res, err := h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: c.State.Mind.ID, Proposition: prop, Epistemic: concepts.Hypothesis, Source: "brain.associate", Evidence: ev, Alternatives: []string{"coincidence: the two events are unrelated", "a third factor caused both"}, OriginEvent: t.EventID, Causation: t.EventID, Correlation: c.State.CycleID, - Detail: map[string]any{"cause_event": t.Content["cause_event"], "effect_event": t.Content["effect_event"], "gap_seconds": t.Content["gap_seconds"]}}) + Detail: map[string]any{"cause_event": t.Content["cause_event"], "effect_event": t.Content["effect_event"], "gap_seconds": t.Content["gap_seconds"], "relation": string(concepts.PossiblyCausedBy), "basis": "temporal association only"}}) if err != nil { if errors.Is(err, beliefs.ErrBlocked) { c.Emit(events.New{Type: events.Metacognition, Source: "brain.executive", Summary: "hypothesis blocked by operator correction: " + prop, Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "do_not_infer"}}) diff --git a/pkg/concepts/concepts.go b/pkg/concepts/concepts.go index 7625c26..5dcda91 100644 --- a/pkg/concepts/concepts.go +++ b/pkg/concepts/concepts.go @@ -76,7 +76,25 @@ const ( ) // AllRelations lists relations in a stable order. -var AllRelations = []Relation{RelatedTo, CausedBy, Contradicts, Supports, DependsOn, BelongsTo, DiscussedWith, ObservedDuring, DerivedFrom, SimilarTo, Follows} +// Association is not causation. CoOccursWith and TemporallyPrecedes record +// what was observed together or in sequence; PossiblyCausedBy is a causal +// hypothesis awaiting evidence; CausedBy is reserved for established causes +// (an explicit operator statement or a confirmed causal prediction); +// AssertedBy links a proposition to whoever stated it. +const ( + CoOccursWith Relation = "co_occurs_with" + PossiblyCausedBy Relation = "possibly_caused_by" + TemporallyPrecedes Relation = "temporally_precedes" + AssertedBy Relation = "asserted_by" +) + +var AllRelations = []Relation{RelatedTo, CausedBy, Contradicts, Supports, DependsOn, BelongsTo, DiscussedWith, ObservedDuring, DerivedFrom, SimilarTo, Follows, CoOccursWith, PossiblyCausedBy, TemporallyPrecedes, AssertedBy} + +// Causal reports whether a relation asserts an established cause. +func (r Relation) Causal() bool { return r == CausedBy } + +// Hypothetical reports whether a relation is a proposal rather than a finding. +func (r Relation) Hypothetical() bool { return r == PossiblyCausedBy } // Valid reports whether r is known. func (r Relation) Valid() bool { diff --git a/pkg/concepts/relations_test.go b/pkg/concepts/relations_test.go new file mode 100644 index 0000000..2dc6a12 --- /dev/null +++ b/pkg/concepts/relations_test.go @@ -0,0 +1,17 @@ +package concepts + +import "testing" + +func TestRelationSemantics(t *testing.T) { + for _, r := range []Relation{CoOccursWith, PossiblyCausedBy, TemporallyPrecedes, AssertedBy, CausedBy} { + if !r.Valid() { + t.Fatalf("%s should be valid", r) + } + } + if !CausedBy.Causal() || PossiblyCausedBy.Causal() || CoOccursWith.Causal() { + t.Fatal("only caused_by asserts a cause") + } + if !PossiblyCausedBy.Hypothetical() || CausedBy.Hypothetical() { + t.Fatal("possibly_caused_by is the hypothetical form") + } +} diff --git a/tests/epistemics/assertions_test.go b/tests/epistemics/assertions_test.go index eca9cd3..5ccfafc 100644 --- a/tests/epistemics/assertions_test.go +++ b/tests/epistemics/assertions_test.go @@ -136,6 +136,14 @@ func TestAssertionsAreNotBeliefs(t *testing.T) { if gb, ok := beliefFor(genius.BeliefID); ok && gb.Confidence > sb.Confidence { t.Fatalf("sarcastic remark believed more than a plain one: %.2f vs %.2f", gb.Confidence, sb.Confidence) } + // Co-mention produces association edges, never causal ones. + var causal int + h.Deps.DB.QueryRow(ctx, `SELECT count(*) FROM entity_relationships WHERE mind_id=$1 AND relation IN ('caused_by')`, m.ID).Scan(&causal) //nolint:errcheck + var cooccur int + h.Deps.DB.QueryRow(ctx, `SELECT count(*) FROM entity_relationships WHERE mind_id=$1 AND relation='co_occurs_with'`, m.ID).Scan(&cooccur) //nolint:errcheck + if causal != 0 || cooccur == 0 { + t.Fatalf("relations: caused_by=%d co_occurs_with=%d", causal, cooccur) + } // Assertions are append-only. if _, err := h.Deps.DB.Exec(ctx, `UPDATE assertions SET proposition='x' WHERE id=$1`, steve.ID); err == nil { t.Fatal("assertion row was mutable")