package main import ( "encoding/json" "fmt" "math" "math/rand/v2" "os" "path/filepath" "sync" "time" ) // Trust levels. const ( Stranger = 0 // you approve triage and output Regular = 1 // output auto-accepted, marked unreviewed Trusted = 2 // runs on its own ) // Record is a track record for one holder (a crow, or the whole flock) on one task type. type Record struct { Approved int `json:"approved"` Rejected int `json:"rejected"` Streak int `json:"streak"` Level int `json:"level"` } func (r *Record) total() int { return r.Approved + r.Rejected } // apply: slow to earn, quick to lose. func (r *Record) apply(ok bool, promoteAfter int) { if ok { r.Approved++ r.Streak++ if r.Streak >= promoteAfter && r.Level < Trusted { r.Level++ r.Streak = 0 } return } r.Rejected++ r.Streak = 0 if r.Level > Stranger { r.Level-- } } // Requester trust: abuse earns a mild, fading penalty. type Requester struct { Strikes int `json:"strikes"` LastStrike time.Time `json:"last_strike"` CooldownUntil time.Time `json:"cooldown_until"` Recent []time.Time `json:"recent"` } type Ledger struct { Crows map[string]*Record `json:"crows"` // key: crow|task_type Flock map[string]*Record `json:"flock"` // key: task_type Requesters map[string]*Requester `json:"requesters"` path string mu sync.Mutex } func LoadLedger(path string) (*Ledger, error) { l := &Ledger{path: path} data, err := os.ReadFile(path) if err != nil && !os.IsNotExist(err) { return nil, err } if err == nil { if err := json.Unmarshal(data, l); err != nil { return nil, fmt.Errorf("ledger is unreadable: %w", err) } } if l.Crows == nil { l.Crows = map[string]*Record{} } if l.Flock == nil { l.Flock = map[string]*Record{} } if l.Requesters == nil { l.Requesters = map[string]*Requester{} } return l, nil } func (l *Ledger) Save() error { l.mu.Lock() defer l.mu.Unlock() data, err := json.MarshalIndent(l, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { return err } tmp := l.path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, l.path) } func get(m map[string]*Record, k string) *Record { r := m[k] if r == nil { r = &Record{} m[k] = r } return r } // Level blends a crow's own record with the flock's record for the task type. // A new crow flies on the flock's reputation; its own record takes over as it grows. func (l *Ledger) Level(crow, taskType string) int { l.mu.Lock() defer l.mu.Unlock() own := get(l.Crows, crow+"|"+taskType) flock := get(l.Flock, taskType) w := float64(own.total()) / float64(own.total()+5) return int(math.Round(w*float64(own.Level) + (1-w)*float64(flock.Level))) } // TrustChange is one level moving up or down. Holder is a crow name or "flock". type TrustChange struct { Holder, TaskType string From, To int } // Judge records a verdict for every crow involved, and once for the flock. // The flock needs twice the streak to level up. It reports any level changes. func (l *Ledger) Judge(crows []string, taskType string, ok bool, promoteAfter int) []TrustChange { l.mu.Lock() defer l.mu.Unlock() var changes []TrustChange judge := func(holder string, r *Record, need int) { from := r.Level r.apply(ok, need) if r.Level != from { changes = append(changes, TrustChange{Holder: holder, TaskType: taskType, From: from, To: r.Level}) } } for _, c := range crows { judge(c, get(l.Crows, c+"|"+taskType), promoteAfter) } judge("flock", get(l.Flock, taskType), promoteAfter*2) return changes } // MarshalSnapshot returns the ledger as JSON, safe to call while lists are flying. func (l *Ledger) MarshalSnapshot() ([]byte, error) { l.mu.Lock() defer l.mu.Unlock() return json.MarshalIndent(l, "", " ") } func (l *Ledger) requester(id string) *Requester { r := l.Requesters[id] if r == nil { r = &Requester{} l.Requesters[id] = r } return r } // Admit checks a requester at the door: cooldowns, rate cap, and fading grudges. // When it says no, it returns a prank message and how long the cooldown lasts. func (l *Ledger) Admit(id string, now time.Time, g Guardrails) (bool, string, time.Duration) { l.mu.Lock() defer l.mu.Unlock() r := l.requester(id) // Grudges fade: one strike forgiven per clean day. if r.Strikes > 0 { if days := int(now.Sub(r.LastStrike) / (24 * time.Hour)); days > 0 { r.Strikes = max(0, r.Strikes-days) r.LastStrike = r.LastStrike.Add(time.Duration(days) * 24 * time.Hour) } } if now.Before(r.CooldownUntil) { wait := r.CooldownUntil.Sub(now) return false, prank(wait), wait } kept := r.Recent[:0] for _, t := range r.Recent { if now.Sub(t) < time.Minute { kept = append(kept, t) } } r.Recent = kept if len(r.Recent) >= g.MaxRequestsPerMin { wait := strike(r, now, g) return false, prank(wait), wait } r.Recent = append(r.Recent, now) return true, "", 0 } func strike(r *Requester, now time.Time, g Guardrails) time.Duration { r.Strikes++ r.LastStrike = now cd := g.BaseCooldown for i := 1; i < r.Strikes && cd < g.MaxCooldown; i++ { cd *= 2 } cd = min(cd, g.MaxCooldown) r.CooldownUntil = now.Add(cd) return cd } var pranks = []string{ "A crow has left a small gift on your windshield. The flock will hear you again in %s.", "The flock turns its back on you. Try again in %s.", "Something shiny of yours has gone missing. Come back in %s.", "Caw. (Translation: not for another %s.)", } func prank(d time.Duration) string { return fmt.Sprintf(pranks[rand.IntN(len(pranks))], d.Round(time.Second)) }