chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation
This commit is contained in:
1 parent
acfba19fc2
commit
b934571b97
25 files changed
+17590
No files matched your search
@@ -0,0 +1,50 @@
|
||||
# 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).
|
||||
Reference in new issue
Block a user