plural service_down nudges agree with the count (V-534)
Two services down read "Мониторинг сообщает: nginx, paperless лежит." — a list
dropped into the singular sentence. Russian agrees the verb with the subject,
so the noun, the verb and the adjective all have to move.
A family may now carry a second set named <rule>_many, used when {service}
holds more than one name. pluralFamily picks it; a family with no _many set is
returned unchanged, so adding one elsewhere is a data change. Only service_down
has one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
This commit is contained in:
@@ -94,7 +94,7 @@ func (t *NudgeTemplates) PhraseNudge(_ context.Context, c loop.Candidate) (deliv
|
|||||||
// template fits it uses the plain per-rule fallback.
|
// template fits it uses the plain per-rule fallback.
|
||||||
func (t *NudgeTemplates) Nudge(c loop.Candidate) (body, mood string) {
|
func (t *NudgeTemplates) Nudge(c loop.Candidate) (body, mood string) {
|
||||||
rule := c.Rule.Name
|
rule := c.Rule.Name
|
||||||
family := t.family(rule)
|
family := t.pluralFamily(t.family(rule), c)
|
||||||
set, ok := t.file.Rules[family]
|
set, ok := t.file.Rules[family]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fallbackNudge(c), "neutral"
|
return fallbackNudge(c), "neutral"
|
||||||
@@ -155,6 +155,25 @@ func (t *NudgeTemplates) family(rule string) string {
|
|||||||
return "default"
|
return "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pluralFamily swaps in the plural wording when {service} will hold a list.
|
||||||
|
// Russian agrees the verb with the subject, so one set of templates cannot
|
||||||
|
// serve both: "Сервис paperless не отвечает" and "Сервисы nginx, paperless не
|
||||||
|
// отвечают" differ in the noun, the verb and the adjective. Filling a list into
|
||||||
|
// the singular text is the kind of near-miss that reads as machine-written.
|
||||||
|
//
|
||||||
|
// Only service_down has a plural form today. A family with no "_many" set in
|
||||||
|
// the file is returned unchanged, so adding one is a data change.
|
||||||
|
func (t *NudgeTemplates) pluralFamily(family string, c loop.Candidate) string {
|
||||||
|
if len(loop.DownServices(c.State)) < 2 {
|
||||||
|
return family
|
||||||
|
}
|
||||||
|
many := family + "_many"
|
||||||
|
if _, ok := t.file.Rules[many]; ok {
|
||||||
|
return many
|
||||||
|
}
|
||||||
|
return family
|
||||||
|
}
|
||||||
|
|
||||||
// placeholderRE — the {name} slots a template may use.
|
// placeholderRE — the {name} slots a template may use.
|
||||||
var placeholderRE = regexp.MustCompile(`\{([a-z]+)\}`)
|
var placeholderRE = regexp.MustCompile(`\{([a-z]+)\}`)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,45 @@ func TestNudgeNamesTheDownService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Russian agrees the verb with the subject, so a list of services cannot go
|
||||||
|
// into the singular sentence. One down takes the singular set, two or more
|
||||||
|
// take service_down_many.
|
||||||
|
func TestNudgeAgreesWithTheServiceCount(t *testing.T) {
|
||||||
|
nt := newTestTemplates(t, 9)
|
||||||
|
// "упал " keeps its trailing space: "упали" starts with "упал", and the
|
||||||
|
// plural must not read as the singular by prefix.
|
||||||
|
singular := []string{"не отвечает", "недоступен", "лежит", "упал "}
|
||||||
|
plural := []string{"не отвечают", "недоступны", "лежат", "упали"}
|
||||||
|
|
||||||
|
for i := 0; i < 60; i++ {
|
||||||
|
body, _ := nt.Nudge(downCand("paperless"))
|
||||||
|
if !containsAny(body, singular) {
|
||||||
|
t.Fatalf("one down, no singular verb: %q", body)
|
||||||
|
}
|
||||||
|
if containsAny(body, plural) {
|
||||||
|
t.Fatalf("one down, plural wording: %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < 60; i++ {
|
||||||
|
body, _ := nt.Nudge(downCand("nginx", "paperless"))
|
||||||
|
if !containsAny(body, plural) {
|
||||||
|
t.Fatalf("two down, no plural verb: %q", body)
|
||||||
|
}
|
||||||
|
if containsAny(body, singular) {
|
||||||
|
t.Fatalf("two down, singular wording: %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAny(s string, subs []string) bool {
|
||||||
|
for _, sub := range subs {
|
||||||
|
if strings.Contains(s, sub) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Nothing down means no template fits, and the fallback answers rather than
|
// Nothing down means no template fits, and the fallback answers rather than
|
||||||
// the picker inventing a name.
|
// the picker inventing a name.
|
||||||
func TestNudgeServiceDownWithoutFacts(t *testing.T) {
|
func TestNudgeServiceDownWithoutFacts(t *testing.T) {
|
||||||
@@ -93,7 +132,7 @@ func newTestTemplates(t *testing.T, seed int64) *NudgeTemplates {
|
|||||||
|
|
||||||
func TestNudgeTemplatesLoad(t *testing.T) {
|
func TestNudgeTemplatesLoad(t *testing.T) {
|
||||||
nt := newTestTemplates(t, 1)
|
nt := newTestTemplates(t, 1)
|
||||||
for _, rule := range []string{"water", "meal", "break", "service_down", "netdata_critical", "routine", "morning", "default"} {
|
for _, rule := range []string{"water", "meal", "break", "service_down", "service_down_many", "netdata_critical", "routine", "morning", "default"} {
|
||||||
set, ok := nt.file.Rules[rule]
|
set, ok := nt.file.Rules[rule]
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("no templates for %q", rule)
|
t.Errorf("no templates for %q", rule)
|
||||||
@@ -120,7 +159,7 @@ func TestNudgeTemplatesLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
seen[v] = true
|
seen[v] = true
|
||||||
}
|
}
|
||||||
if plain == 0 && rule != "routine" && rule != "morning" && rule != "service_down" {
|
if plain == 0 && rule != "routine" && rule != "morning" && !strings.HasPrefix(rule, "service_down") {
|
||||||
t.Errorf("%s: every variant needs a placeholder value", rule)
|
t.Errorf("%s: every variant needs a placeholder value", rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"Hand-written Russian nudges. Edit the wording here, no Go changes needed.",
|
"Hand-written Russian nudges. Edit the wording here, no Go changes needed.",
|
||||||
"Rules: she is feminine about herself, he is a man addressed as ты. Never вы/вас/ваш, never plural imperatives (выпейте), never он/его about him.",
|
"Rules: she is feminine about herself, he is a man addressed as ты. Never вы/вас/ваш, never plural imperatives (выпейте), never он/его about him.",
|
||||||
"One short sentence. No questions, no emoji, no pet names, no emotional support.",
|
"One short sentence. No questions, no emoji, no pet names, no emotional support.",
|
||||||
"Placeholders: {since} how long it has been (only used when it is at least an hour), {service} the service name, {what} the routine name. A variant whose placeholder has no value is skipped, so every rule needs at least one variant with no placeholder. The exception is routine and morning: those only exist for rules like routine:таблетки that always carry a name, and a routine nudge that drops the name is useless.",
|
"Placeholders: {since} how long it has been (only used when it is at least an hour), {service} the service name, {what} the routine name. A variant whose placeholder has no value is skipped, so every rule needs at least one variant with no placeholder. The exception is routine, morning and service_down: those only exist for rules that always carry a name, and one that drops the name is useless.",
|
||||||
|
"A rule may carry a second set named <rule>_many, used when {service} holds more than one name. Russian agrees the verb with the subject, so the plural needs its own wording rather than a list dropped into the singular sentence. Only service_down has one.",
|
||||||
"mood must be one of: neutral, happy, thinking, tired, confused."
|
"mood must be one of: neutral, happy, thinking, tired, confused."
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -67,6 +68,19 @@
|
|||||||
"Сервис {service} не отвечает, посмотри логи."
|
"Сервис {service} не отвечает, посмотри логи."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"service_down_many": {
|
||||||
|
"mood": "neutral",
|
||||||
|
"variants": [
|
||||||
|
"Сервисы {service} не отвечают.",
|
||||||
|
"{service} упали — сервисы не отвечают.",
|
||||||
|
"{service} не отвечают, сервисы нужно поднимать.",
|
||||||
|
"Сервисы {service} недоступны.",
|
||||||
|
"Проверь {service}: сервисы не отвечают.",
|
||||||
|
"Сервисы {service} лежат, нужно смотреть.",
|
||||||
|
"Мониторинг сообщает: {service} лежат.",
|
||||||
|
"Сервисы {service} не отвечают, посмотри логи."
|
||||||
|
]
|
||||||
|
},
|
||||||
"netdata_critical": {
|
"netdata_critical": {
|
||||||
"mood": "neutral",
|
"mood": "neutral",
|
||||||
"variants": [
|
"variants": [
|
||||||
|
|||||||
Reference in New Issue
Block a user