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 }