Address PR review comments on 50, 52, 53, 54, 59, 61

Seven fixes, each answering a line comment on the stack.

**Weather no longer invents Moscow** (PR 50). extractWeatherLocation returned
the string "Moscow" when he named no city and voice.weather.default_location
was unset — a made-up answer presented as fact, which is the one thing maven
must never do. It returns "" now and the query path says it does not know.

**Digest statuses are a defined type** (PR 50). DigestStatus string plus the
three constants, so a rule name cannot reach the status column.

**Quiet-mode negation is not adjacency** (PR 53). The OFF list carried
{"не","тих"}, an adjacency pattern, so "не надо тихий режим" missed OFF, hit
the ON pattern {"тих","режим"}, and asking for quiet mode to stop turned it
on. Negators are scanned over the whole utterance now, with the two ON phrases
that are themselves built on "не" excluded. "тихий режим выключи" works too,
which it did not before.

**Pattern stability uses a median band** (PR 54). max/min over the extremes
asked whether every gap resembles every other gap, so 7,7,7,7,20 — four clean
weeks and one holiday — was thrown away at a ratio of 2.9. Each interval is
now tested against the median and 70% must be in band, and the reported
interval is the median of the in-band ones, so a holiday no longer drags a
weekly habit to "every 9.6 days". The reviewer's 5,8,10,3 is still rejected.

**The weekday profile stops reciting everyday habits** (PR 59). "What do I do
on Saturdays?" answered "you drink water" — true, and useless, because it is
equally true of every other day. Activities that are habits on six or more
weekdays move to Profile.Everyday and are read back as daily habits instead of
as an answer about that day.

**Russian phrase tables move out of Go** (PR 59, PR 61). The behaviour glosses
and weekday names, and the task capture/urgency/list vocabulary, are now
behavior_ru.json and task_phrases.json, embedded with go:embed. Single-binary
deploy is unchanged; wording edits are no longer source diffs.

**nginx template stops taking nginx down** (PR 52). Two host-side failure
modes, both plausible causes of today's crash. The $connection_upgrade map is
fatal when duplicated, so it moved to its own nginx-upgrade-map.conf with a
grep-first note. And `listen 10.42.0.1:80` fails with EADDRNOTAVAIL when wg0
is not up yet, so nginx exits on a reboot that beats WireGuard — the header
now documents net.ipv4.ip_nonlocal_bind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
kami
2026-08-01 12:50:47 +04:00
parent 927e46bca3
commit 7f42cc73be
17 changed files with 621 additions and 146 deletions
+64 -30
View File
@@ -53,15 +53,27 @@ type Activity struct {
TypicalAt time.Duration
}
// Profile — the counted behaviour model. Weekly holds the activities that
// recur on a given weekday, Overall the ones that recur at all.
// Profile — the counted behaviour model.
//
// Weekly holds only the activities that DISTINGUISH a weekday: things he does
// on Tuesdays and not on most other days. Everyday holds the ones that recur
// across the week, and All holds both. The split exists because the two answer
// different questions, and conflating them produced the failure that named
// this: asked what he does on Saturdays, maven replied "ты пьёшь воду".
type Profile struct {
Since time.Time
Until time.Time
Weekly map[time.Weekday][]Activity
All []Activity
Since time.Time
Until time.Time
Weekly map[time.Weekday][]Activity
Everyday []Activity
All []Activity
}
// EverydaySpan — the number of weekdays an activity must be a habit on before
// it stops counting as characteristic of any one of them. Six of seven, not
// five: a weekday-only rhythm spans exactly five, and "по будням ты
// тренируешься" is a real answer about Tuesday. Six days a week is not.
const EverydaySpan = 6
// MinHabitDays — how many distinct days an activity must appear on before maven
// will call it usual. Two is the smallest number that can distinguish a habit
// from a one-off; below that she says she does not know yet, which is true.
@@ -161,9 +173,36 @@ func BuildProfile(obs []Observation, now time.Time) Profile {
}
p.All = harvest(all)
// How many weekdays each key is a habit on. An activity that recurs on
// most days of the week is a daily habit, and naming it as an answer to
// "что я обычно делаю по субботам?" is a non-answer: "ты пьёшь воду" is
// true of Saturday and of every other day, so it says nothing about
// Saturday. Those are held in Everyday and read back separately.
span := map[string]int{}
harvested := map[time.Weekday][]Activity{}
for wd, m := range weekly {
if acts := harvest(m); len(acts) > 0 {
p.Weekly[wd] = acts
acts := harvest(m)
harvested[wd] = acts
for _, a := range acts {
span[a.Key]++
}
}
for wd, acts := range harvested {
var distinct []Activity
for _, a := range acts {
if span[a.Key] >= EverydaySpan {
continue
}
distinct = append(distinct, a)
}
if len(distinct) > 0 {
p.Weekly[wd] = distinct
}
}
for _, a := range p.All {
if span[a.Key] >= EverydaySpan {
p.Everyday = append(p.Everyday, a)
}
}
return p
@@ -193,33 +232,28 @@ func medianInt(xs []int) int {
return (s[mid-1] + s[mid]) / 2
}
// weekdayRU — accusative, as "по вторникам" and "в среду" both need it read
// back. Index is time.Weekday.
var weekdayRU = [...]string{"воскресеньям", "понедельникам", "вторникам", "средам", "четвергам", "пятницам", "субботам"}
// weekdayRU and activityRU are loaded from the embedded behavior_ru.json;
// see behavior_ru.go.
// activityRU glosses the loop's known fact keys. An unknown key is read back
// verbatim: it is what the store holds, and inventing a Russian phrase for a key
// maven does not recognise would be putting words in his mouth.
var activityRU = map[string]string{
"water": "пьёшь воду",
"meal": "ешь",
"sleep": "спишь",
"break": "делаешь перерыв",
"shower": "принимаешь душ",
"walk": "гуляешь",
"pills": "пьёшь витамины",
"workout": "тренируешься",
}
// FormatWeekdayRU reads back what he usually does on a given weekday.
// FormatWeekdayRU reads back what DISTINGUISHES a given weekday.
// Second person singular and informal, as she speaks TO him.
//
// When nothing distinguishes it, she says so and names the daily habits as
// daily habits instead of passing them off as an answer about that day. The
// previous version had no such distinction and answered "что я делаю по
// субботам?" with "ты пьёшь воду" — true, useless, and phrased as if Saturday
// were the reason.
func (p Profile) FormatWeekdayRU(wd time.Weekday) string {
acts := p.Weekly[wd]
day := weekdayRU[int(wd)%7]
if len(acts) == 0 {
return fmt.Sprintf("по %s у меня пока нет ничего постоянного.", day)
acts := p.Weekly[wd]
if len(acts) > 0 {
return fmt.Sprintf("по %s ты обычно %s.", day, joinActivities(acts))
}
return fmt.Sprintf("по %s ты обычно %s.", day, joinActivities(acts))
if len(p.Everyday) > 0 {
return fmt.Sprintf("по %s у тебя нет ничего особенного — то же, что и в остальные дни: %s.",
day, joinActivities(p.Everyday))
}
return fmt.Sprintf("по %s у меня пока нет ничего постоянного.", day)
}
// FormatOverallRU reads back the habits that hold across the whole week.