feat(graph): association is not causation
Adds co_occurs_with, temporally_precedes, possibly_caused_by and asserted_by to the relation vocabulary with Causal and Hypothetical predicates. Entities mentioned together now co-occur rather than being 'related'; association hypotheses record possibly_caused_by and their basis (temporal association only); caused_by is reserved for explicit operator statements and nothing automatic writes it. Documented in docs/graph.md and checked end to end.
This commit is contained in:
+11
-1
@@ -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".
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"}})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user