{
  "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."
    }
  ]
}
