Add web server, event stream, and Rive contract
- crows serve: HTTP API, Server-Sent Events per list, web approvals with a 10-minute fail-safe timeout; localhost only by default - Shared pipeline for command line and server; trust.changed events - EVENTS.md: server/UI contract; RIVE_CONTRACT.md: data-binding spec for the animator (one Crow artboard for v1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
12 files changed
+1320
-129
No files matched your search
@@ -123,15 +123,37 @@ func (l *Ledger) Level(crow, taskType string) int {
|
||||
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.
|
||||
func (l *Ledger) Judge(crows []string, taskType string, ok bool, promoteAfter int) {
|
||||
// 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()
|
||||
for _, c := range crows {
|
||||
get(l.Crows, c+"|"+taskType).apply(ok, promoteAfter)
|
||||
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})
|
||||
}
|
||||
}
|
||||
get(l.Flock, taskType).apply(ok, promoteAfter*2)
|
||||
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 {
|
||||
@@ -144,7 +166,8 @@ func (l *Ledger) requester(id string) *Requester {
|
||||
}
|
||||
|
||||
// Admit checks a requester at the door: cooldowns, rate cap, and fading grudges.
|
||||
func (l *Ledger) Admit(id string, now time.Time, g Guardrails) (bool, string) {
|
||||
// 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)
|
||||
@@ -157,7 +180,8 @@ func (l *Ledger) Admit(id string, now time.Time, g Guardrails) (bool, string) {
|
||||
}
|
||||
}
|
||||
if now.Before(r.CooldownUntil) {
|
||||
return false, prank(r.CooldownUntil.Sub(now))
|
||||
wait := r.CooldownUntil.Sub(now)
|
||||
return false, prank(wait), wait
|
||||
}
|
||||
kept := r.Recent[:0]
|
||||
for _, t := range r.Recent {
|
||||
@@ -167,10 +191,11 @@ func (l *Ledger) Admit(id string, now time.Time, g Guardrails) (bool, string) {
|
||||
}
|
||||
r.Recent = kept
|
||||
if len(r.Recent) >= g.MaxRequestsPerMin {
|
||||
return false, prank(strike(r, now, g))
|
||||
wait := strike(r, now, g)
|
||||
return false, prank(wait), wait
|
||||
}
|
||||
r.Recent = append(r.Recent, now)
|
||||
return true, ""
|
||||
return true, "", 0
|
||||
}
|
||||
|
||||
func strike(r *Requester, now time.Time, g Guardrails) time.Duration {
|
||||
|
||||
Reference in new issue
Block a user