@@ -21,10 +21,14 @@ const (
// CheckHisGender — the other half of the persona rule: SHE is feminine, HE
// is male. "ты давно не отдыхала" addresses the operator as a woman.
CheckHisGender = "hisgender"
// CheckAddress — she talks TO him, informally, one to one. Not "вы", not
// "он". See the comment block above checkAddress.
CheckAddress = "address"
)
// CheckNames — report order.
var CheckNames = [ ] string { CheckMood , CheckLang , CheckLength , CheckFeminine , CheckHisGender , CheckCringe , CheckOnTopic }
var CheckNames = [ ] string { CheckMood , CheckLang , CheckLength , CheckFeminine , CheckHisGender , CheckAddress , CheckCringe , CheckOnTopic }
// Result — one check on one message.
type Result struct {
@@ -57,6 +61,7 @@ func RunChecks(c Case, body, mood string) []Result {
checkLength ( body ) ,
checkFeminine ( body ) ,
checkHisGender ( body ) ,
checkAddress ( body ) ,
checkCringe ( body ) ,
checkOnTopic ( c , body ) ,
}
@@ -302,6 +307,159 @@ func prevWord(words []string, i int) string {
return ""
}
// --- how she addresses him ------------------------------------------------
//
// Persona hard constraint: Maven speaks TO him, informally, one to one. The
// phrasing eval produced two breaks of it, and both scored clean:
//
// - "Приходите… Жду вас" — the formal plural. Correct is ты/тебя/тебе and a
// singular imperative ("приходи", "жду тебя").
// - "Он не ел 11 дней" — she talks ABOUT him, in the third person, as if
// reporting to somebody else. Correct is "ты не ел 11 дней".
//
// Like checkHisGender this is a keyword + suffix heuristic, NOT a parser. Every
// hit prints the word it tripped on, so a false alarm is obvious at a glance and
// can be dismissed.
//
// Part 1, formal address. Two signals:
// - the "вы" pronoun family, matched as whole words, so there is nothing to
// exclude — "вы" and "вас" are never anything else.
// - a plural verb ending: -ите/-ете/-йте/-ьте ("приходите", "выпейте",
// "не забудьте", "хотите"). Nouns in the prepositional case share those
// endings ("в интернете", "в свете"), so a word right after a preposition is
// skipped. That is the whole exclusion list, on purpose: a bigger one would
// start swallowing real imperatives.
//
// Part 2, third person. "он" is perfectly fine when the message really is about
// somebody or something else ("сервис упал, он не отвечает"). The way to tell
// them apart: a legitimate third person has an ANTECEDENT — the thing it refers
// to was named earlier in the message. So "он" is only flagged when nothing
// before it in the message could be that thing.
//
// Where this gives up, plainly:
// - it only looks BACKWARD. "Он не отвечает, сервис упал" names the subject
// after the pronoun and is flagged wrongly.
// - any noun earlier in the message counts as an antecedent, even when it is
// not one ("после обеда он не ел" reads as legitimate and is missed). The
// common time words are stoplisted so the usual nudge opening does not
// blind it, but a message with any other noun in front still slips through.
// This is the check's real hole; widening it further would start flagging
// legitimate third-party messages, so it stops here.
// - a message that opens with "ты" and only later slips into "он" is missed,
// because "ты" itself is skipped but the words around it are not.
// - formal address outside these endings (short adjectives, "вашими" style
// forms not listed) is missed.
// addressWordRE also takes Latin words, because "him"/"he" is the same break in
// English.
var addressWordRE = regexp . MustCompile ( ` [\p { Cyrillic}]+|[a-zA-Z]+|[,.;:!?…—-] ` )
// formalPronouns — the "вы" family. Whole-word match, so no false hits.
var formalPronouns = map [ string ] bool {
"вы" : true , "вас" : true , "вам" : true , "вами" : true ,
"ваш" : true , "ваша" : true , "ваше" : true , "ваши" : true ,
"вашего" : true , "вашей" : true , "вашему" : true , "вашим" : true ,
"вашими" : true , "вашу" : true ,
}
// prepositions — used twice: to skip prepositional-case nouns that look like
// plural verbs, and as words that cannot be what "он" refers to.
var prepositions = map [ string ] bool {
"в" : true , "во" : true , "на" : true , "о " : true , "об" : true , "обо" : true ,
"при" : true , "по" : true , "за" : true , "из" : true , "с " : true , "со" : true ,
"к" : true , "ко" : true , "до" : true , "от" : true , "у " : true , "над" : true ,
"под" : true , "про" : true , "без" : true , "для" : true , "через" : true ,
}
// pluralVerb reports whether a word looks like a plural/formal verb form:
// "приходите", "выпейте", "забудьте", "хотите".
func pluralVerb ( w string ) bool {
if len ( [ ] rune ( w ) ) < 5 {
return false
}
return strings . HasSuffix ( w , "ите" ) || strings . HasSuffix ( w , "ете" ) ||
strings . HasSuffix ( w , "йте" ) || strings . HasSuffix ( w , "ьте" )
}
// thirdPersonHim — pronouns that would be talking about him instead of to him.
var thirdPersonHim = map [ string ] bool {
"он" : true , "его" : true , "ему" : true , "него" : true , "нему" : true , "ним" : true ,
"he" : true , "him" : true , "his" : true ,
}
// notAnAntecedent — words that cannot be the thing "он" refers to: pronouns,
// particles, conjunctions, adverbs of time. If only these come before "он", the
// message never named a third party and "он" is him.
var notAnAntecedent = map [ string ] bool {
"не" : true , "ни" : true , "и" : true , "а " : true , "но" : true , "да" : true ,
"же" : true , "бы" : true , "ли" : true , "вот" : true , "уже" : true ,
"ещё" : true , "еще" : true , "тоже" : true , "там" : true , "тут" : true ,
"здесь" : true , "это" : true , "что" : true , "как" : true , "когда" : true ,
"чтобы" : true , "потому" : true , "сейчас" : true , "потом" : true ,
// Time words. A nudge almost always opens with one ("сегодня он не ел"),
// and without them the very next word is read as the person being talked
// about, so the check misses the exact break it was written for.
"сегодня" : true , "вчера" : true , "завтра" : true , "послезавтра" : true ,
"утром" : true , "днём" : true , "днем" : true , "вечером" : true , "ночью" : true ,
"опять" : true , "снова" : true , "весь" : true , "всю" : true , "целый" : true ,
"я" : true , "мне" : true , "меня" : true , "мной" : true , "мы" : true , "нас" : true ,
"ты" : true , "тебя" : true , "тебе" : true , "тобой" : true ,
"твой" : true , "твоя" : true , "твоё" : true , "твое" : true , "твои" : true , "твою" : true ,
}
// looksPastVerb — a past-tense verb needs a subject of its own, so it is not an
// antecedent either. Keeps "сервис упал, он не отвечает" working off "сервис".
func looksPastVerb ( w string ) bool {
if len ( [ ] rune ( w ) ) < 3 {
return false
}
return strings . HasSuffix ( w , "л" ) || strings . HasSuffix ( w , "ла" ) ||
strings . HasSuffix ( w , "ло" ) || strings . HasSuffix ( w , "ли" )
}
func checkAddress ( body string ) Result {
words := addressWordRE . FindAllString ( strings . ToLower ( body ) , - 1 )
for i , w := range words {
if formalPronouns [ w ] {
return Result { CheckAddress , false ,
fmt . Sprintf ( "formal %q — she says ты/тебя/тебе" , w ) }
}
if pluralVerb ( w ) && ! ( i > 0 && prepositions [ words [ i - 1 ] ] ) {
return Result { CheckAddress , false ,
fmt . Sprintf ( "plural imperative %q — she uses the singular" , w ) }
}
}
for i , w := range words {
if ! thirdPersonHim [ w ] {
continue
}
named := false
for j := 0 ; j < i ; j ++ {
p := words [ j ]
if ! unicode . Is ( unicode . Cyrillic , [ ] rune ( p ) [ 0 ] ) && ! isLatinWord ( p ) {
continue // punctuation
}
if notAnAntecedent [ p ] || prepositions [ p ] || thirdPersonHim [ p ] || looksPastVerb ( p ) {
continue
}
named = true
break
}
if ! named {
return Result { CheckAddress , false ,
fmt . Sprintf ( "third person %q with nobody else named — she talks to him, not about him" , w ) }
}
}
return Result { CheckAddress , true , "" }
}
func isLatinWord ( w string ) bool {
r := [ ] rune ( w ) [ 0 ]
return ( r >= 'a' && r <= 'z' ) || ( r >= 'A' && r <= 'Z' )
}
// --- the cringe checks ---------------------------------------------------
//
// "Think Jarvis without the cringe part". DESIGN.md § Non-goals: "Not a