159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
|
|
package science
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"sort"
|
||
|
|
|
||
|
|
"github.com/tegwick/fluid-core/internal/contract"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Competition groups rival explanations of the same pressure.
|
||
|
|
//
|
||
|
|
// FluidAPIStandards.md section 19 requires this and section 65 of the same
|
||
|
|
// document asks the Daimon to preserve uncertainty where evidence is
|
||
|
|
// insufficient. A framework that forced one explanation forward would make
|
||
|
|
// premature commitment the default and hide the alternatives that a later
|
||
|
|
// reader might have preferred.
|
||
|
|
type Competition struct {
|
||
|
|
GroupID string `json:"group_id"`
|
||
|
|
Members []contract.HypothesisID `json:"members"`
|
||
|
|
Resolved bool `json:"resolved"`
|
||
|
|
Preferred contract.HypothesisID `json:"preferred,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// ErrNotCompeting reports hypotheses that are not in the same group.
|
||
|
|
var ErrNotCompeting = errors.New("hypotheses are not in the same competition group")
|
||
|
|
|
||
|
|
// Compete places hypotheses into a competition group.
|
||
|
|
//
|
||
|
|
// Membership is symmetric: every member lists every other. A one-way link
|
||
|
|
// would let a reader looking at one hypothesis miss that a rival exists, which
|
||
|
|
// is precisely the mistake the group is meant to prevent.
|
||
|
|
func (s *HypothesisStore) Compete(ctx context.Context, groupID string, ids []contract.HypothesisID, actor contract.Actor) (Competition, error) {
|
||
|
|
if groupID == "" {
|
||
|
|
return Competition{}, errors.New("a competition group needs an id")
|
||
|
|
}
|
||
|
|
if len(ids) < 2 {
|
||
|
|
return Competition{}, errors.New("a competition group needs at least two hypotheses")
|
||
|
|
}
|
||
|
|
|
||
|
|
seen := map[contract.HypothesisID]struct{}{}
|
||
|
|
members := make([]contract.HypothesisID, 0, len(ids))
|
||
|
|
for _, id := range ids {
|
||
|
|
if _, dup := seen[id]; dup {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, err := s.Get(ctx, id); err != nil {
|
||
|
|
return Competition{}, fmt.Errorf("cannot add %s to %s: %w", id, groupID, err)
|
||
|
|
}
|
||
|
|
seen[id] = struct{}{}
|
||
|
|
members = append(members, id)
|
||
|
|
}
|
||
|
|
sort.Slice(members, func(i, j int) bool { return members[i] < members[j] })
|
||
|
|
|
||
|
|
for _, id := range members {
|
||
|
|
h, err := s.Get(ctx, id)
|
||
|
|
if err != nil {
|
||
|
|
return Competition{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
alternatives := make([]contract.HypothesisID, 0, len(members)-1)
|
||
|
|
for _, other := range members {
|
||
|
|
if other != id {
|
||
|
|
alternatives = append(alternatives, other)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
h.Competition = &contract.FluidHypothesisCompetition{
|
||
|
|
GroupID: groupID,
|
||
|
|
Alternatives: alternatives,
|
||
|
|
}
|
||
|
|
if err := s.put(ctx, h); err != nil {
|
||
|
|
return Competition{}, err
|
||
|
|
}
|
||
|
|
if err := s.event(ctx, h, "HYPOTHESIS_COMPETING", actor,
|
||
|
|
fmt.Sprintf("joined competition group %s against %v", groupID, alternatives), nil); err != nil {
|
||
|
|
return Competition{}, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return Competition{GroupID: groupID, Members: members}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group returns the members of a competition group.
|
||
|
|
func (s *HypothesisStore) Group(ctx context.Context, groupID string) (Competition, error) {
|
||
|
|
all, err := s.List(ctx, "")
|
||
|
|
if err != nil {
|
||
|
|
return Competition{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
c := Competition{GroupID: groupID}
|
||
|
|
for _, h := range all {
|
||
|
|
if h.Competition == nil || h.Competition.GroupID != groupID {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
c.Members = append(c.Members, h.ID)
|
||
|
|
if h.State == contract.FluidHypothesisStateACCEPTED {
|
||
|
|
c.Resolved = true
|
||
|
|
c.Preferred = h.ID
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Slice(c.Members, func(i, j int) bool { return c.Members[i] < c.Members[j] })
|
||
|
|
|
||
|
|
if len(c.Members) == 0 {
|
||
|
|
return c, fmt.Errorf("no hypotheses in competition group %q", groupID)
|
||
|
|
}
|
||
|
|
return c, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Resolve accepts one member of a competition and supersedes the rest.
|
||
|
|
//
|
||
|
|
// The losers become SUPERSEDED rather than REJECTED. Rejection says the
|
||
|
|
// explanation was wrong; superseded says a better one won. The distinction
|
||
|
|
// matters when the winner is later refuted and someone goes looking for what
|
||
|
|
// else had been considered.
|
||
|
|
func (s *HypothesisStore) Resolve(ctx context.Context, groupID string, winner contract.HypothesisID, actor contract.Actor, reason string) (Competition, error) {
|
||
|
|
if reason == "" {
|
||
|
|
return Competition{}, errors.New("resolving a competition requires a reason")
|
||
|
|
}
|
||
|
|
|
||
|
|
group, err := s.Group(ctx, groupID)
|
||
|
|
if err != nil {
|
||
|
|
return group, err
|
||
|
|
}
|
||
|
|
|
||
|
|
found := false
|
||
|
|
for _, id := range group.Members {
|
||
|
|
if id == winner {
|
||
|
|
found = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
return group, fmt.Errorf("%w: %s is not in %s", ErrNotCompeting, winner, groupID)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, id := range group.Members {
|
||
|
|
if id == winner {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
h, err := s.Get(ctx, id)
|
||
|
|
if err != nil {
|
||
|
|
return group, err
|
||
|
|
}
|
||
|
|
if h.State == contract.FluidHypothesisStateSUPERSEDED ||
|
||
|
|
h.State == contract.FluidHypothesisStateREJECTED {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, err := s.Transition(ctx, id, contract.FluidHypothesisStateSUPERSEDED, actor,
|
||
|
|
fmt.Sprintf("superseded by %s in competition %s: %s", winner, groupID, reason)); err != nil {
|
||
|
|
return group, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
group.Resolved = true
|
||
|
|
group.Preferred = winner
|
||
|
|
return group, nil
|
||
|
|
}
|