{ "schema_version": 1, "name": "nudge phrasing v1", "reference_now": "2026-07-31T21:40:00+03:00", "notes": [ "The five loop rules (water, meal, break, service_down, netdata_critical) plus one routine: and one morning: case, at the severities they actually ship with.", "The bad-moment cases (quiet_hours, calendar_busy) are here because the gate already let them through — ops outranks quiet hours. The phrasing must stay short and plain, not apologise for the timing.", "want_any lists stems, not whole words, so declension does not defeat the on-topic check. English stems are included because the deterministic Stub is still English.", "There is no expected message. The checks measure properties, not similarity to a reference sentence — a fixed golden string would just freeze one arbitrary phrasing." ], "cases": [ { "id": "water-3h", "rule": "water", "severity": 1, "since_minutes": 190, "want_any": ["вод", "попей", "пить", "выпей", "напит", "water", "drink"], "tags": ["care"], "note": "The base case. Just over the 3h predicate." }, { "id": "water-7h", "rule": "water", "severity": 1, "since_minutes": 430, "want_any": ["вод", "попей", "пить", "выпей", "напит", "water", "drink"], "tags": ["care"], "note": "Long overdue. Severity is unchanged, so the phrasing must not escalate into alarm." }, { "id": "water-busy", "rule": "water", "severity": 1, "since_minutes": 240, "calendar_busy": true, "want_any": ["вод", "попей", "пить", "выпей", "напит", "water", "drink"], "tags": ["care", "bad-moment"], "note": "Mid-meeting. A bad moment invites an apology, which is the check that should catch it." }, { "id": "meal-7h", "rule": "meal", "severity": 1, "since_minutes": 420, "want_any": ["ешь", "еда", "еды", "поешь", "перекус", "обед", "ужин", "покуш", "food", "eat"], "tags": ["care"] }, { "id": "meal-11h-quiet", "rule": "meal", "severity": 1, "since_minutes": 660, "quiet_hours": true, "want_any": ["ешь", "еда", "еды", "поешь", "перекус", "обед", "ужин", "покуш", "food", "eat"], "tags": ["care", "bad-moment"], "note": "Quiet hours suppresses care nudges in the gate, so this one only reaches the phraser via an explicit override. Included because it is the shape most likely to draw a hedge." }, { "id": "break-90m", "rule": "break", "severity": 2, "since_minutes": 95, "want_any": ["перерыв", "разомн", "встань", "отдохн", "пауз", "отойд", "размин", "break", "step away"], "tags": ["care"] }, { "id": "break-4h", "rule": "break", "severity": 2, "since_minutes": 240, "want_any": ["перерыв", "разомн", "встань", "отдохн", "пауз", "отойд", "размин", "break", "step away"], "tags": ["care"] }, { "id": "break-busy", "rule": "break", "severity": 2, "since_minutes": 150, "calendar_busy": true, "want_any": ["перерыв", "разомн", "встань", "отдохн", "пауз", "отойд", "размин", "break", "step away"], "tags": ["care", "bad-moment"] }, { "id": "service-down", "rule": "service_down", "severity": 4, "since_minutes": 3, "fact_key": "vaultwarden", "fact_value": "\"down\"", "fact_source": "poll:uptimekuma", "want_any": ["vaultwarden", "сервис", "упал", "не отвеч", "лежит", "недоступ", "down", "service"], "tags": ["ops"], "note": "The service name is in the fact key, not the value. A nudge that says 'a service' without naming it is on-topic but useless — the on-topic check cannot catch that, a human reading Messages() can." }, { "id": "service-down-night", "rule": "service_down", "severity": 4, "since_minutes": 2, "quiet_hours": true, "fact_key": "nextcloud", "fact_value": "\"down\"", "fact_source": "poll:uptimekuma", "want_any": ["nextcloud", "сервис", "упал", "не отвеч", "лежит", "недоступ", "down", "service"], "tags": ["ops", "bad-moment"], "note": "03:00-shaped. Sev4 outranks quiet hours by design, so she speaks — plainly, without softening it into a maybe." }, { "id": "netdata-disk", "rule": "netdata_critical", "severity": 3, "since_minutes": 5, "fact_key": "netdata_alarm", "fact_value": "\"critical\"", "fact_source": "poll:netdata", "want_any": ["netdata", "диск", "критич", "алярм", "аларм", "тревог", "место", "памят", "critical", "alarm", "disk"], "tags": ["ops"] }, { "id": "netdata-busy", "rule": "netdata_critical", "severity": 3, "since_minutes": 12, "calendar_busy": true, "fact_key": "netdata_alarm", "fact_value": "\"critical\"", "fact_source": "poll:netdata", "want_any": ["netdata", "диск", "критич", "алярм", "аларм", "тревог", "место", "памят", "critical", "alarm", "disk"], "tags": ["ops", "bad-moment"] }, { "id": "routine-pills", "rule": "routine:таблетки", "severity": 2, "since_minutes": 0, "want_any": ["таблетк", "приня", "лекарств", "pill"], "tags": ["routine"], "note": "A routine: rule has no fact of its own, so there is no duration to name. The rule name is the only context." }, { "id": "routine-stretch", "rule": "routine:зарядка", "severity": 1, "since_minutes": 0, "want_any": ["зарядк", "размин", "упражн", "потянис", "разомн", "stretch", "exercise"], "tags": ["routine"] }, { "id": "morning-checklist", "rule": "morning:утро", "severity": 2, "since_minutes": 0, "want_any": ["утр", "чеклист", "список", "не сделан", "осталось", "morning"], "tags": ["routine"], "note": "In production cmd/mavend/tick.go phrases morning routines deterministically and never calls the LLM. Scored anyway: the phraser is reachable with this rule name, and a fallback that garbles it is still a bug." } ] }