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:
SmokeyandClaude Opus 5.5 committed 2026-10-27 08:31:27 -06:00
1 parent 7ae7f670ff
commit 940c74672a
12 files changed
+1320 -129

No files matched your search

+34 -9
View File
@@ -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 {