temporal_freshness then authority_pairing; unresolved keeps (not confirmed). bin/facts/audit contradict. Gitea #29.
121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
// Package facts is cgo-free evidence rules (D16 contradictions).
|
|
package facts
|
|
|
|
import "strconv"
|
|
|
|
const (
|
|
ConfConfirmed = "confirmed"
|
|
ConfHypothesis = "hypothesis"
|
|
|
|
RuleUnresolved = "unresolved"
|
|
RuleTemporalFreshness = "temporal_freshness"
|
|
RuleAuthorityPairing = "authority_pairing"
|
|
RuleTwoSource = "two_source"
|
|
RuleSingleSource = "single_source"
|
|
|
|
KindRuntime = "runtime"
|
|
KindConfig = "config"
|
|
KindNarrative = "narrative"
|
|
)
|
|
|
|
// Source is one independent pointer on a yes or no side.
|
|
type Source struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
When string `json:"when,omitempty"`
|
|
Stale bool `json:"stale,omitempty"`
|
|
}
|
|
|
|
// Claim is one assertion with yes/no evidence lists.
|
|
type Claim struct {
|
|
Text string `json:"text"`
|
|
Yes []Source `json:"yes"`
|
|
No []Source `json:"no"`
|
|
}
|
|
|
|
// Result is audit output. Confirmed=false means `(not confirmed)`.
|
|
type Result struct {
|
|
Text string `json:"text"`
|
|
Confidence string `json:"confidence"`
|
|
Confirmed bool `json:"confirmed"`
|
|
Rule string `json:"rule"`
|
|
Winner string `json:"winner,omitempty"`
|
|
YesN int `json:"yes"`
|
|
NoN int `json:"no"`
|
|
}
|
|
|
|
func independent(ss []Source) int {
|
|
seen := map[string]struct{}{}
|
|
for i, s := range ss {
|
|
id := s.ID
|
|
if id == "" {
|
|
id = s.Kind + "#" + strconv.Itoa(i)
|
|
}
|
|
seen[id] = struct{}{}
|
|
}
|
|
return len(seen)
|
|
}
|
|
|
|
func freshN(ss []Source) int {
|
|
n := 0
|
|
for _, s := range ss {
|
|
if !s.Stale {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func strongN(ss []Source) int {
|
|
n := 0
|
|
for _, s := range ss {
|
|
if s.Kind == KindRuntime || s.Kind == KindConfig {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func out(c Claim, conf, rule, winner string) Result {
|
|
return Result{
|
|
Text: c.Text,
|
|
Confidence: conf,
|
|
Confirmed: conf == ConfConfirmed,
|
|
Rule: rule,
|
|
Winner: winner,
|
|
YesN: independent(c.Yes),
|
|
NoN: independent(c.No),
|
|
}
|
|
}
|
|
|
|
// Adjudicate applies D16: ≥2 yes vs ≥2 no stays hypothesis until a rule fires.
|
|
// Order: temporal_freshness, then authority_pairing (A/B beats narrative C).
|
|
func Adjudicate(c Claim) Result {
|
|
yesN := independent(c.Yes)
|
|
noN := independent(c.No)
|
|
if yesN < 2 || noN < 2 {
|
|
if yesN >= 2 {
|
|
return out(c, ConfConfirmed, RuleTwoSource, "yes")
|
|
}
|
|
if noN >= 2 {
|
|
return out(c, ConfConfirmed, RuleTwoSource, "no")
|
|
}
|
|
return out(c, ConfHypothesis, RuleSingleSource, "")
|
|
}
|
|
yf, nf := freshN(c.Yes), freshN(c.No)
|
|
if yf >= 2 && nf < 2 {
|
|
return out(c, ConfConfirmed, RuleTemporalFreshness, "yes")
|
|
}
|
|
if nf >= 2 && yf < 2 {
|
|
return out(c, ConfConfirmed, RuleTemporalFreshness, "no")
|
|
}
|
|
ys, ns := strongN(c.Yes), strongN(c.No)
|
|
if ys >= 2 && ns < 2 {
|
|
return out(c, ConfConfirmed, RuleAuthorityPairing, "yes")
|
|
}
|
|
if ns >= 2 && ys < 2 {
|
|
return out(c, ConfConfirmed, RuleAuthorityPairing, "no")
|
|
}
|
|
return out(c, ConfHypothesis, RuleUnresolved, "")
|
|
}
|