Files
talk/.superpowers/sdd/plan/task-5-report.md
T

51 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task-5-Report: Rate-Limit + Ruhezeiten
Status: **DONE** · Branch `feat/v1` · Basis `dc94f45` (Task 4)
Commits: `b20e6dd` (config) → `5385a4b` (queue) → `b56c8ab` (api/talkd/cli) → `5979d45` (Prozessgrenzen-Tests) → `24ceff3` (README). Kein Push, `.scratch/` unangetastet.
## Timing-Semantik (verbindlich, wer wartet / wer verfällt / was now darf)
Rangordnung: **mute (API, immer) > now > Ruhezeit > Rate-Limit.**
| Situation | low | normal | high | now:true |
| --- | --- | --- | --- | --- |
| Rate-Fenster voll (Start der letzten Ansage < `queue.rate_limit` her) | **verworfen** — schon beim Eintritt, 202 `{id:"",discarded:"rate_limit"}`; kam es bei freiem Fenster rein, verfällt es beim Worker-Durchlauf (Log-Zeile) | **parkt** in der Queue, spielt zum Slot (`rate_limit.next_slot` in /status; Slot-Wecker weckt den Worker) | mit `allow_high_burst=true` (Default): **spielt sofort** (bewusster Verstoß); `false`: parkt wie normal | **spielt sofort** — umgeht auch das Rate-Limit (korrigierte Brief-Entscheidung umgesetzt) |
| Ruhezeit aktiv (`quiet_hours`, lokal, über Mitternacht) | **verworfen**, 202 `{discarded:"quiet_hours"}` | dito | dito — Burst ist ein Rate-Recht, kein Ruhe-Recht | **spielt sofort** — bricht auch die Ruhezeit (dokumentierter Ausbruch) |
| `quiet_mode="toast"` | / | / | / | / : statt Verwurf **Toast-Ersetzung**, 202 `{discarded:"quiet_hours",toast:true}` |
| Fenster frei + keine Ruhezeit | spielt | spielt | spielt | spielt + springt vor |
Weitere Regeln, dokumentiert und getestet:
- **Jede gestartete Ansage bucht das Fenster neu** (`Reserve` vor dem Abspielen) — auch eine per now oder high-burst ausgebrochene und auch eine fehlgeschlagene (kaputter Provider darf nicht zum Maschinengewehr werden). Fenster = Mindestabstand zwischen Ansage-**Starts**.
- While parked bleibt ein normal/high-Item voll sichtbar (`queue_ids`, `--wait` funktioniert); neu eintreffende now-/burst-fähige Items werden vorgezogen (Rank-Ordnung unverändert: now > high > normal > low, FIFO je Level).
- `--wait` auf ein verworfenes Item endet sauber (ID verschwindet aus der Queue — dasselbe Muster wie bei cancel).
## Umsetzung
- `internal/queue/quiet.go` (neu): `QuietHours` — Parse `HH:MM-HH:MM` (leer = aus, Start==End = aus), `Active` (Tag-/Nachtfenster, Start inklusiv, End exklusiv), `Until` (Fenster-Ende für /status), `String` (Roundtrip).
- `internal/queue/rate.go`: `RateLimiter` (aus T4, `Next()` liefert jetzt Nullzeit bei nie gebuchtem Fenster laut Doku), dazu `Laws` + reine `decide`-Ordnung, `Admission` (Enqueue-Ergebnis mit Verwurfs-Grund), `LawState` (/status-Sicht), Grund-Konstanten `rate_limit`/`quiet_hours`.
- `internal/queue/queue.go`: Loop erweitert (nicht umgeschrieben — Rank/Serialisierung/Mute unangetastet): `readyLocked`/`takeLocked` (parken ohne Entnahme), `armWakeLocked` (Slot-Wecker broadcastet die Cond; Wait-Bedingung prüft die Gesetze neu → verlorene Signale harmlos), `Enqueue` entscheidet Ruhezeit/low-Budget schon beim Eintritt (damit die 202 den Grund nennen kann). Lock-Ordnung Queue → RateLimiter durchgehend gewahrt; Race-Detektor grün.
- `internal/config`: `[queue] rate_limit = "30s"` (Dauer-String, `"0s"` = unbegrenzt; TOML-Decode via `time.Duration`-TextUnmarshaler, verifiziert), `allow_high_burst` (Default true; partielle `[queue]`-Tabellen behalten Defaults — getestet), `quiet_hours`/`quiet_mode` (Default aus/discard). Env: `TALK_RATE_LIMIT`, `TALK_ALLOW_HIGH_BURST`, `TALK_QUIET_HOURS` (`off` = aus), `TALK_QUIET_MODE`. Validierung gegen `queue.ParseQuietHours` (eine Quelle; Import config→queue, kein Zyklus).
- `internal/api`: `/speak` antwortet bei Verwurf 202 `{id:"",discarded:…}` (+`toast:true`) — analog zum Mute-Muster, kein Fehler; `/status` additiv `rate_limit {interval, allow_high_burst, next_slot}` + `quiet_hours {window, active, mode, until}` (RFC3339 bzw. null). Kein Feld gebrochen.
- `cmd/talkd`: verwired `Laws` aus der Config, Startup-Log nennt das Gesetz.
- CLI: `say` meldet `discarded: <grund>` (+ ` (toast)`) plain auf stdout mit Exit 0 (wie `muted`); `talk status` zeigt `rate-limit: … (high-burst …), nächster slot …` und `ruhezeit: … (modus …, aktiv bis …/inaktiv)`.
- README: kompletter Abschnitt zu Semantik + Config/Env + Rangordnung; Endpunkt-Tabelle aktualisiert.
## Tests
- **Prozessgrenze** (`test/ratelimit_test.go`, echte Binaries, Fake-Provider, sink=null, ephemeral Ports): kurzes Fenster (1 s) → 5 Ansagen: genau 1 spielt sofort, low verfällt beim Eintritt (HTTP **und** CLI-Output), normal parkt (queue_ids + next_slot sichtbar) und spielt frühestens zum Slot (Timing am Fake gemessen, ≥ 850 ms Abstand); high-burst an (spielt im Fenster) / aus (parkt, cancel räumt, Daemon gesund — das Warten-Aus mit langem Fenster deckt der Park-Test); `now` bricht durch Rate-Limit **und** Ruhezeit; Ruhezeit-Fenster künstlich um die lokale Jetztzeit (±1 min) → discard (auch high, kein Provider-Kontakt, Log-Zeile, until in /status) und toast (Toast-Protokoll-Zeile); `/status`- + `talk status`-Felder (Defaults 30 s/Burst an/Slot frei, gebuchter Slot nach Ansage).
- **Unit** (`internal/queue`): QuietHours-Parse/Active/Until/String (tischgesteuert inkl. Mitternacht); Queue-Gesetze mit gesteuerter Spielerin (low verfällt, normal parkt + spielt, burst ein/aus, now-Ausbruch + Folgebuchung, Ruhezeit discard/toast auch für high, LawState).
- **Config** (`internal/config`): Defaults, volle/partielle TOML, Env-Overrides inkl. `off`, klare Fehler für Müll.
- Harness: Basis-Env `TALK_RATE_LIMIT=0s` — die Task-04-Semantik-Tests (Serialisierung, Priorität, …) erwarten dichtes Durchspielen und blieben unverändert; Gesetz-Tests überschreiben bewusst.
Verifikation: `gofmt -l .` leer · `go vet ./...` grün · `go test ./...` grün · `go test -race -count=1 ./...` grün (queue+test zusätzlich `-count=2` zur Flakiness-Probe) · `go build ./...` grün · Cross-Compile-Smoke `CGO_ENABLED=0 GOOS=darwin/windows GOARCH=amd64` für talk+talkd erfolgreich.
## Entscheidungen & Hinweise
1. **`now:true` umgeht auch das Rate-Limit** — die CORRECTION im Brief ist bindend umgesetzt und überall (Code-Kommentare, README, Tests) konsistent dokumentiert; ein now-Bruch startet trotzdem ein neues Fenster.
2. **Ruhezeiten sind per Default aus** (leer). Ein Default-Fenster „18:00–08:00" würde jeden Cron/Test in den Abendstunden stilllegen (CI läuft zu jeder Uhrzeit) — Opt-in wie Mute, im Config-Kommentar begründet.
3. **`quiet_mode="toast"` ist derzeit eine Daemon-Protokoll-Zeile** über den Notification-Pfad (Log-Writer), keine OS-Benachrichtigung — die Brief-Vorgabe „log line via notification path if trivial, else document limitation": der Notification-Pfad existiert noch nicht (internal/notify ist doc-only), also die dokumentierte Einschränkung. Die Ersetzung sitzt im Gesetz an genau einer Stelle (`verdictToast`), die OS-Anbindung ersetzt nur das Log-Ziel.
4. **Degraded Mode respektiert weiterhin nur Mute**, nicht Rate-Limit/Ruhezeit (Task-4-Umfang unverändert; der Brief fordert das Gesetz für den Daemon). Der Lock-File-Pfad bleibt sequenziell-best-effort. Für V1 akzeptiert; falls gewünscht, als Folge-Task.
5. Verwurf beim **Eintritt** (low über Budget, Ruhezeit) vs. **Worker-Durchlauf**: Eintritt, damit die 202-Antwort den Grund nennen kann (so der Brief); was bei freiem Fenster eintritt und erst später verfällt, verfällt im Worker mit Log-Zeile — beides getestet bzw. in der Semantik-Tabelle dokumentiert.
6. Stale Slot-Wecker nach Close sind harmlos (Broadcast ohne Waiter; Code-Kommentar erläutert es).