Files
Pete/internal/classifier/gating.go
2026-05-22 17:25:27 -07:00

78 lines
3.1 KiB
Go

package classifier
import "regexp"
// GatingResult indicates the outcome of keyword-based gating for Tier 2 stories.
type GatingResult int
const (
GatingPost GatingResult = iota // Stage 1: definite post
GatingDiscard // Stage 2: definite discard
GatingAmbiguous // Stage 3: escalate to LLM
)
// Stage 1: Definite post patterns — stories with clear public significance.
// Patterns use multi-word context to reduce false positives (e.g. "Star Wars" won't match).
var postPatterns = []*regexp.Regexp{
// Named heads of state / major international bodies
regexp.MustCompile(`(?i)\b(president|prime minister|chancellor|pope)\s+\w+`),
regexp.MustCompile(`(?i)\b(UN|United Nations|WHO|NATO|EU|European Union|IMF|World Bank)\b`),
// Casualty language with numbers
regexp.MustCompile(`(?i)\b\d+\s*(dead|killed|injured|casualties|wounded)\b`),
regexp.MustCompile(`(?i)\b(dead|killed|injured|casualties|wounded)\s*\d+\b`),
// Disaster language — require a location-like context word nearby
regexp.MustCompile(`(?i)\b(earthquake|hurricane|typhoon|tsunami|wildfire|tornado)\b.*\b(hit|struck|destroyed|devastated|kills|deaths)\b`),
// Conflict language — require multi-word phrases to avoid "Star Wars", "culture war"
regexp.MustCompile(`(?i)\b(military invasion|armed coup|airstrike\s+\w+|assassination\s+\w+|car bombing|suicide bombing)\b`),
regexp.MustCompile(`(?i)\bwar\s+(in|on|with|against|between)\b`),
// Crisis language — require context word nearby
regexp.MustCompile(`(?i)\b(state of emergency|economic crisis|economic collapse|disease outbreak|pandemic declared|pandemic phase|famine)\b`),
// Major financial scale
regexp.MustCompile(`(?i)\$\d+\s*(bn|tn|billion|trillion)\b`),
regexp.MustCompile(`(?i)\b(trillion|billion)\s*(dollar|euro|pound)\b`),
}
// Stage 2: Definite discard patterns — stories unlikely to warrant posting.
var discardPatterns = []*regexp.Regexp{
// Local/municipal scope
regexp.MustCompile(`(?i)\b(city council|municipal|town hall|zoning|local police)\b`),
// Sports scores and routine results
regexp.MustCompile(`(?i)\b(final score|match result|league standings|defeated|innings)\b`),
regexp.MustCompile(`(?i)\b\d+-\d+\s*(win|loss|defeat|victory)\b`),
// Entertainment/celebrity lifestyle
regexp.MustCompile(`(?i)\b(red carpet|award show|celebrity gossip|dating rumor|relationship drama|album release)\b`),
// Routine corporate earnings
regexp.MustCompile(`(?i)\b(quarterly earnings|Q[1-4] results|earnings call|fiscal quarter)\b`),
}
// RunGating evaluates Tier 2 gating stages on headline + lede.
// Returns the gating result and a reason string for logging.
func RunGating(headline, lede string) (GatingResult, string) {
combined := headline + " " + lede
// Stage 1: Definite post
for _, re := range postPatterns {
if re.MatchString(combined) {
return GatingPost, "stage1: " + re.String()
}
}
// Stage 2: Definite discard
for _, re := range discardPatterns {
if re.MatchString(combined) {
return GatingDiscard, "stage2: " + re.String()
}
}
// Stage 3: Ambiguous — escalate to LLM
return GatingAmbiguous, "stage3: no keyword match"
}