Files
Andy Dixon 7821d89cf5 feat(beliefs): temporal scope and validity; state changes supersede rather than contradict
Beliefs gain a temporal scope (persistent, transient or unknown, classified
deterministically from the proposition), an observation time and, for
transient states, a validity interval. When a newer belief conflicts with
an older transient one observed at least a minute earlier, the older belief
is closed (valid_until set, status superseded, a supersedes link, a version
and an event saying 'state changed') with its score untouched: 'the door
was open at 10:00' stays true of 10:00 when it is closed at 10:05.
Simultaneous conflicting reports and persistent propositions contradict as
before. Introspection lists superseded states with their validity.
Migration 0020 is additive.
2026-08-09 21:42:19 +01:00

56 lines
2.6 KiB
Go

package beliefs
import (
"regexp"
"strings"
"time"
)
// Temporal scopes. Persistent propositions are true or false without an
// interval ("atlas is the primary host", "Andy prefers Go"). Transient ones
// describe a state that can change ("the door is open", "server A is down")
// and carry a validity interval. Unknown is the honest default when the
// classifier cannot tell.
const (
ScopePersistent = "persistent"
ScopeTransient = "transient"
ScopeUnknown = "unknown"
)
// MinStateChangeGap is the shortest interval between two observations for a
// later, conflicting observation of a transient state to count as a state
// change rather than a contradiction. Two reports of the same moment that
// disagree are a contradiction; reports minutes apart are a change.
const MinStateChangeGap = time.Minute
var (
reTransient = regexp.MustCompile(`(?i)\b(?:is|are|was|were|remains|seems|looks|appears|has been|have been|is currently|is now)\s+(?:not\s+|still\s+|no longer\s+)?(?:down|up|open|closed|offline|online|running|stopped|stuck|failing|degraded|full|empty|busy|idle|locked|unlocked|available|unavailable|slow|broken|healthy|unhealthy|overloaded|unreachable|reachable|responding|paused|active|inactive|pending|in progress|at capacity|on call|away|present|absent|late|early|hot|cold|warm|crossed)\b|\b(?:right now|currently|at the moment|at \d{1,2}:\d{2}|between \d|since \d|until \d|today|tonight|this morning|this afternoon|for the last|in the last)\b`)
rePersistent = regexp.MustCompile(`(?i)\b(?:is (?:a|an|the)\b|are (?:a|an|the)\b|was born|prefers?|likes?|dislikes?|always|never|is named|is called|is the name|belongs to|owns|is made of|is located in|is based in|works at|works for|is part of|means|stands for|is written in|uses|runs on|depends on)\b`)
)
// ClassifyScope decides, deterministically, whether a proposition names a
// transient state, a persistent fact, or neither recognisably.
func ClassifyScope(proposition string) string {
p := strings.TrimSpace(proposition)
switch {
case reTransient.MatchString(p):
return ScopeTransient
case rePersistent.MatchString(p):
return ScopePersistent
}
return ScopeUnknown
}
// StateChange reports whether a newer belief should close an older,
// conflicting one as a change of state rather than contest it: both must be
// transient, and the newer observation must be later by at least the gap.
func StateChange(older, newer Belief) bool {
if older.TemporalScope != ScopeTransient || newer.TemporalScope != ScopeTransient {
return false
}
if older.ObservedAt == nil || newer.ObservedAt == nil {
return false
}
return newer.ObservedAt.Sub(*older.ObservedAt) >= MinStateChangeGap
}