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.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user